diff --git a/docs/review/api/alfa-rules.api.md b/docs/review/api/alfa-rules.api.md index 2b42fc5726..f850e7e9e1 100755 --- a/docs/review/api/alfa-rules.api.md +++ b/docs/review/api/alfa-rules.api.md @@ -51,7 +51,7 @@ import { Tag } from '@siteimprove/alfa-act'; import { Text } from '@siteimprove/alfa-dom'; // @public -const _default: Rule.Atomic, never, Element>; +const _default: Rule.Atomic>; // @public (undocumented) const _default_10: Rule.Atomic, never, Element>; @@ -84,7 +84,7 @@ const _default_18: Rule.Atomic, never, Element>; const _default_19: Rule.Atomic>, Question.Metadata, Group>>; // @public -const _default_2: Rule.Atomic>; +const _default_2: Rule.Atomic, Question.Metadata, Node | Array_2>>; // @public (undocumented) const _default_20: Rule.Atomic, never, Element>; @@ -117,7 +117,7 @@ const _default_28: Rule.Atomic, Question.Metadata, Element const _default_29: Rule.Atomic, Question.Metadata, Element>; // @public -const _default_3: Rule.Atomic, Question.Metadata, Node | Array_2>>; +const _default_3: Rule.Atomic; // @public (undocumented) const _default_30: Rule.Atomic, Question.Metadata, Element>; @@ -149,8 +149,8 @@ const _default_38: Rule.Atomic, Question.Metadata, Element // @public (undocumented) const _default_39: Rule.Composite, Question.Metadata, Element>; -// @public -const _default_4: Rule.Atomic; +// @public (undocumented) +const _default_4: Rule.Atomic, never, Element>; // @public (undocumented) const _default_40: Rule.Atomic, Question.Metadata, Element>; @@ -227,7 +227,7 @@ const _default_61: Rule.Atomic, never, Element>; // @public (undocumented) const _default_62: Rule.Atomic; -// @public (undocumented) +// @public const _default_63: Rule.Atomic, never, Element>; // @public (undocumented) @@ -332,6 +332,13 @@ const _default_93: Rule.Atomic, never, Element>; // @public (undocumented) const _default_94: Rule.Atomic, never, Element>; +declare namespace deprecatedRules { + export { + _default_4 as DR62 + } +} +export { deprecatedRules } + // @public (undocumented) export namespace Diagnostic { const // Warning: (ae-forgotten-export) The symbol "ClippingAncestors" needs to be exported by the entry point index.d.ts @@ -360,7 +367,7 @@ export namespace Diagnostic { const // Warning: (ae-forgotten-export) The symbol "DistinguishingStyles" needs to be exported by the entry point index.d.ts // // (undocumented) - isDistinguishingStylesExperimental: typeof DistinguishingStyles_2.isDistinguishingStyles; + isDistinguishingStylesDeprecated: typeof DistinguishingStyles_2.isDistinguishingStyles; const // Warning: (ae-forgotten-export) The symbol "MatchingClasses" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -393,10 +400,9 @@ export namespace Diagnostic { declare namespace experimentalRules { export { - _default as ER62, - _default_2 as ER87, - _default_3 as R82, - _default_4 as R109 + _default as ER87, + _default_2 as R82, + _default_3 as R109 } } export { experimentalRules } @@ -697,6 +703,7 @@ export namespace Stability { } const Experimental: Stability<"experimental">; const Stable: Stability<"stable">; + const Deprecated: Stability<"deprecated">; } // @public (undocumented) diff --git a/packages/alfa-rules/src/common/act/diagnostic.ts b/packages/alfa-rules/src/common/act/diagnostic.ts index b4603e8ac7..077b448938 100755 --- a/packages/alfa-rules/src/common/act/diagnostic.ts +++ b/packages/alfa-rules/src/common/act/diagnostic.ts @@ -1,10 +1,10 @@ -import { DistinguishingStyles as DistinguishingStylesExperimental } from "../../sia-er62/diagnostics"; +import { DistinguishingStyles } from "../../sia-r62/diagnostics"; import { Languages } from "../../sia-r109/rule"; import { LabelAndName } from "../../sia-r14/rule"; import { RoleAndRequiredAttributes } from "../../sia-r16/rule"; import { SameNames } from "../../sia-r56/rule"; -import { DistinguishingStyles } from "../../sia-r62/rule"; import { MatchingClasses } from "../../sia-r65/diagnostics"; +import { DistinguishingStyles as DeprecatedDistinguishingStyles } from "../../sia-dr62/rule"; import { DeprecatedElements } from "../../sia-r70/rule"; import { WithDeclaration } from "../../sia-r75/rule"; import { ClippingAncestors } from "../../sia-r83/rule"; @@ -35,8 +35,8 @@ export namespace Diagnostic { export const { isDistinguishingStyles } = DistinguishingStyles; - export const { isDistinguishingStyles: isDistinguishingStylesExperimental } = - DistinguishingStylesExperimental; + export const { isDistinguishingStyles: isDistinguishingStylesDeprecated } = + DeprecatedDistinguishingStyles; export const { isMatchingClasses } = MatchingClasses; diff --git a/packages/alfa-rules/src/deprecated.ts b/packages/alfa-rules/src/deprecated.ts new file mode 100755 index 0000000000..8158310fed --- /dev/null +++ b/packages/alfa-rules/src/deprecated.ts @@ -0,0 +1 @@ +export { default as DR62 } from "./sia-dr62/rule"; diff --git a/packages/alfa-rules/src/experimental.ts b/packages/alfa-rules/src/experimental.ts index 478743cb00..8b8bc98416 100755 --- a/packages/alfa-rules/src/experimental.ts +++ b/packages/alfa-rules/src/experimental.ts @@ -1,4 +1,3 @@ -export { default as ER62 } from "./sia-er62/rule"; export { default as ER87 } from "./sia-er87/rule"; export { default as R82 } from "./sia-r82/rule"; export { default as R109 } from "./sia-r109/rule"; diff --git a/packages/alfa-rules/src/index.ts b/packages/alfa-rules/src/index.ts index a9e1d9c478..5913a95bae 100644 --- a/packages/alfa-rules/src/index.ts +++ b/packages/alfa-rules/src/index.ts @@ -7,6 +7,9 @@ export * from "./tags"; import * as experimentalRules from "./experimental"; export { experimentalRules }; +import * as deprecatedRules from "./deprecated"; +export { deprecatedRules }; + import * as rules from "./rules"; /** diff --git a/packages/alfa-rules/src/sia-dr62/rule.ts b/packages/alfa-rules/src/sia-dr62/rule.ts new file mode 100644 index 0000000000..8d86d018b4 --- /dev/null +++ b/packages/alfa-rules/src/sia-dr62/rule.ts @@ -0,0 +1,548 @@ +import { Rule, Diagnostic } from "@siteimprove/alfa-act"; +import { DOM } from "@siteimprove/alfa-aria"; +import { Cache } from "@siteimprove/alfa-cache"; +import { Color } from "@siteimprove/alfa-css"; +import { Device } from "@siteimprove/alfa-device"; +import { Element, Node, Text } from "@siteimprove/alfa-dom"; +import { Equatable } from "@siteimprove/alfa-equatable"; +import { Hash, Hashable } from "@siteimprove/alfa-hash"; +import { Serializable } from "@siteimprove/alfa-json"; +import { Map } from "@siteimprove/alfa-map"; +import { Option, None } from "@siteimprove/alfa-option"; +import { Predicate } from "@siteimprove/alfa-predicate"; +import { Refinement } from "@siteimprove/alfa-refinement"; +import { Err, Ok, Result } from "@siteimprove/alfa-result"; +import { Context } from "@siteimprove/alfa-selector"; +import { Sequence } from "@siteimprove/alfa-sequence"; +import { Set } from "@siteimprove/alfa-set"; +import { Property, Style } from "@siteimprove/alfa-style"; +import { Criterion } from "@siteimprove/alfa-wcag"; +import { Page } from "@siteimprove/alfa-web"; + +import * as json from "@siteimprove/alfa-json"; + +import { expectation } from "../common/act/expectation"; +import { isWhitespace } from "../common/predicate"; +import { Scope, Stability } from "../tags"; + +import { Serialise } from "./serialise"; + +const { hasRole } = DOM; +const { isElement } = Element; +const { or, not, test } = Predicate; +const { and } = Refinement; +const { + hasBorder, + hasBoxShadow, + hasComputedStyle, + hasOutline, + hasTextDecoration, + isVisible, +} = Style; +const { isText } = Text; + +export default Rule.Atomic.of({ + uri: "https://alfa.siteimprove.com/rules/sia-r62", + requirements: [Criterion.of("1.4.1")], + tags: [Scope.Component, Stability.Deprecated], + evaluate({ device, document }) { + let containers: Map = Map.empty(); + + return { + applicability() { + return visit(document, None); + + function* visit( + node: Node, + container: Option + ): Iterable { + if (isElement(node)) { + // If the element is a semantic link, it might be applicable. + if ( + test( + hasRole(device, (role) => role.is("link")), + node + ) + ) { + if ( + container.isSome() && + node + .descendants(Node.flatTree) + .some(and(isText, isVisible(device))) + ) { + containers = containers.set(node, container.get()); + return yield node; + } + } + + // Otherwise, if the element is a

element with non-link text + // content then start collecting applicable elements. + else if ( + test( + and(hasRole(device, "paragraph"), hasNonLinkText(device)), + node + ) + ) { + container = Option.of(node); + } + } + + for (const child of node.children(Node.fullTree)) { + yield* visit(child, container); + } + } + }, + + expectations(target) { + const nonLinkElements = containers + .get(target) + .get() + .inclusiveDescendants(Node.fullTree) + .filter(and(isElement, hasNonLinkText(device))); + + const linkElements = target + // All descendants of the link. + .inclusiveDescendants(Node.fullTree) + .filter(isElement) + // Plus those ancestors who don't include non-link text. + .concat( + target + .ancestors(Node.fullTree) + .takeWhile(and(isElement, not(hasNonLinkText(device)))) + ); + + const hasDistinguishingStyle = (context?: Context) => + Set.from( + linkElements.map((link) => + // If the link element is distinguishable from at least one + // non-link element, this is good enough. + // Note that ACT rules draft requires the link-element to be + // distinguishable from *all* non-link elements in order to be good. + nonLinkElements.some((container) => + isDistinguishable(container, device, context)(link) + ) + ? Ok.of(ComputedStyles.from(link, device, context)) + : Err.of(ComputedStyles.from(link, device, context)) + ) + ) + .toArray() + // sort the Ok before the Err, relative order doesn't matter. + .sort((a, b) => (b.isOk() ? 1 : -1)); + + // The context needs to be set on the *target*, not on its ancestors + // or descendants + const isDefaultDistinguishable = hasDistinguishingStyle(); + + const isHoverDistinguishable = hasDistinguishingStyle( + Context.hover(target) + ); + + const isFocusDistinguishable = hasDistinguishingStyle( + Context.focus(target) + ); + + return { + 1: expectation( + // If at least one link element is good, this is enough. The sorting + // guarantees it is first in the array. + isDefaultDistinguishable[0].isOk() && + isHoverDistinguishable[0].isOk() && + isFocusDistinguishable[0].isOk(), + () => + Outcomes.IsDistinguishable( + isDefaultDistinguishable, + isHoverDistinguishable, + isFocusDistinguishable + ), + () => + Outcomes.IsNotDistinguishable( + isDefaultDistinguishable, + isHoverDistinguishable, + isFocusDistinguishable + ) + ), + }; + }, + }; + }, +}); + +export namespace Outcomes { + // We could tweak typing to ensure that isDistinguishable only accepts Ok and + // that isNotDistinguishable has at least one Err. + // This would requires changing the expectation since it does not refine + // and is thus probably not worth the effort. + export const IsDistinguishable = ( + defaultStyles: Iterable>, + hoverStyles: Iterable>, + focusStyles: Iterable> + ) => + Ok.of( + DistinguishingStyles.of( + `The link is distinguishable from the surrounding text`, + defaultStyles, + hoverStyles, + focusStyles + ) + ); + + export const IsNotDistinguishable = ( + defaultStyles: Iterable>, + hoverStyles: Iterable>, + focusStyles: Iterable> + ) => + Err.of( + DistinguishingStyles.of( + `The link is not distinguishable from the surrounding text`, + defaultStyles, + hoverStyles, + focusStyles + ) + ); +} + +const hasNonLinkTextCache = Cache.empty(); + +function hasNonLinkText(device: Device): Predicate { + return function hasNonLinkText(element) { + return hasNonLinkTextCache.get(element, () => { + // If we are already below a link, escape. + if ( + element.inclusiveAncestors(Node.flatTree).some( + and( + isElement, + hasRole(device, (role) => role.is("link")) + ) + ) + ) { + return false; + } + + const children = element.children(Node.flatTree); + + // If we've found text with more than whitespaces, we're done. + if ( + children.some( + and( + isText, + and(isVisible(device), (text) => !isWhitespace(text.data)) + ) + ) + ) { + return true; + } + + // Otherwise, go down. + return children + .filter(isElement) + .reject(hasRole(device, (role) => role.is("link"))) + .some(hasNonLinkText); + }); + }; +} + +function isDistinguishable( + container: Element, + device: Device, + context: Context = Context.empty() +): Predicate { + return or( + // Things like text decoration and backgrounds risk blending with the + // container element. We therefore need to check if these can be distinguished + // from what the container element might itself set. + hasDistinguishableTextDecoration(container, device, context), + hasDistinguishableBackground(container, device, context), + + hasDistinguishableFontWeight(container, device, context), + hasDistinguishableVerticalAlign(container, device, context), + // We consider the mere presence of borders or outlines on the element as + // distinguishable features. There's of course a risk of these blending with + // other features of the container element, such as its background, but this + // should hopefully not happen (too often) in practice. When it does, we + // risk false negatives. + hasOutline(device, context), + hasBorder(device, context), + hasBoxShadow(device, context) //Checks for color != transparent and spread => 0 + ); +} + +function hasDistinguishableTextDecoration( + container: Element, + device: Device, + context?: Context +): Predicate { + return (element) => + test(not(hasTextDecoration(device, context)), container) && + test(hasTextDecoration(device, context), element); +} + +/** + * Check if an element has a distinguishable background from the given container + * element. + * + * @remarks + * This predicate currently only considers `background-color` and + * `background-image` as a possibly distinguishable background. Other + * `background-*` properties should ideally also be considered. + * + * Additionally, this predicate do not handle transparency in the topmost layer. + * The exact same (partly transparent) `background-color` or `background-image` + * could be on top of a different (opaque) background and thus creates a + * difference. However, in these cases the (lower layer) distinguishing + * background would be on an ancestor of the link but of no non-link text (in + * order to be distinguishing), so should be found when looking at the ancestors + * of the link. + * + * Lastly, this does not account for absolutely positioned backgrounds from + * random nodes in the DOM. Using these to push an image below links in + * paragraph sounds so crazy (from a sheer code maintenance point of view) that + * this hopefully won't be a problem. + */ +function hasDistinguishableBackground( + container: Element, + device: Device, + context?: Context +): Predicate { + const colorReference = Style.from(container, device, context).computed( + "background-color" + ).value; + + const imageReference = Style.from(container, device, context).computed( + "background-image" + ).value; + + return or( + hasComputedStyle( + "background-color", + not( + // If the background is fully transparent, we assume it will end up + // being the same as the container. Intermediate backgrounds may change + // that, but these would need to be set on ancestor of the link and of + // no non-link text, so will be caught in one of the other comparisons. + (color) => Color.isTransparent(color) || color.equals(colorReference) + ), + device, + context + ), + // Any difference in `background-image` is considered enough. If different + // `background-image` ultimately yield the same background (e.g. the same + // image at two different URLs), this creates false negatives. + hasComputedStyle( + "background-image", + not((image) => image.equals(imageReference)), + device, + context + ) + ); +} + +/** + * Check if an element has a different font weight than its container. + * + * This is brittle and imperfect but removes a strong pain point until we find + * a better solution. + */ +function hasDistinguishableFontWeight( + container: Element, + device: Device, + context?: Context +): Predicate { + const reference = Style.from(container, device, context).computed( + "font-weight" + ).value; + + return hasComputedStyle( + "font-weight", + not((weight) => weight.equals(reference)), + device, + context + ); +} + +function hasDistinguishableVerticalAlign( + container: Element, + device: Device, + context?: Context +): Predicate { + const reference = Style.from(container, device, context).computed( + "vertical-align" + ).value; + + return hasComputedStyle( + "vertical-align", + not((alignment) => alignment.equals(reference)), + device, + context + ); +} + +type Name = Property.Name | Property.Shorthand.Name; + +export class ComputedStyles implements Equatable, Hashable, Serializable { + public static of( + style: Iterable = [] + ): ComputedStyles { + return new ComputedStyles(Map.from(style)); + } + + private readonly _style: Map; + + private constructor(style: Map) { + this._style = style; + } + + public get style(): Map { + return this._style; + } + + public equals(value: ComputedStyles): boolean; + + public equals(value: unknown): value is this; + + public equals(value: unknown): boolean { + return value instanceof ComputedStyles && value._style.equals(this._style); + } + + public hash(hash: Hash): void { + this._style.hash(hash); + } + + public toJSON(): ComputedStyles.JSON { + return { + style: this._style.toJSON(), + }; + } +} + +export namespace ComputedStyles { + export interface JSON { + [key: string]: json.JSON; + style: Map.JSON; + } + + export function isComputedStyles(value: unknown): value is ComputedStyles { + return value instanceof ComputedStyles; + } + + export function from( + element: Element, + device: Device, + context: Context = Context.empty() + ): ComputedStyles { + const style = Style.from(element, device, context); + + const border = (["color", "style", "width"] as const).map((property) => + Serialise.borderShorthand(style, property) + ); + + return ComputedStyles.of( + [ + ...border, + ["color", Serialise.getLonghand(style, "color")] as const, + ["font-weight", Serialise.getLonghand(style, "font-weight")] as const, + [ + "vertical-align", + Serialise.getLonghand(style, "vertical-align"), + ] as const, + ["background", Serialise.background(style)] as const, + ["outline", Serialise.outline(style)] as const, + ["text-decoration", Serialise.textDecoration(style)] as const, + ["box-shadow", Serialise.boxShadow(style)] as const, + ].filter(([_, value]) => value !== "") + ); + } +} + +/** + * @internal + */ +export class DistinguishingStyles extends Diagnostic { + public static of( + message: string, + defaultStyles: Iterable> = Sequence.empty(), + hoverStyles: Iterable> = Sequence.empty(), + focusStyles: Iterable> = Sequence.empty() + ): DistinguishingStyles { + return new DistinguishingStyles( + message, + Sequence.from(defaultStyles), + Sequence.from(hoverStyles), + Sequence.from(focusStyles) + ); + } + + private readonly _defaultStyles: Sequence>; + private readonly _hoverStyles: Sequence>; + private readonly _focusStyles: Sequence>; + + private constructor( + message: string, + defaultStyles: Sequence>, + hoverStyles: Sequence>, + focusStyles: Sequence> + ) { + super(message); + this._defaultStyles = defaultStyles; + this._hoverStyles = hoverStyles; + this._focusStyles = focusStyles; + } + + public get defaultStyles(): Iterable> { + return this._defaultStyles; + } + + public get hoverStyles(): Iterable> { + return this._hoverStyles; + } + + public get focusStyles(): Iterable> { + return this._focusStyles; + } + + public equals(value: DistinguishingStyles): boolean; + + public equals(value: unknown): value is this; + + public equals(value: unknown): boolean { + return ( + value instanceof DistinguishingStyles && + value._defaultStyles.equals(this._defaultStyles) && + value._hoverStyles.equals(this._hoverStyles) && + value._focusStyles.equals(this._focusStyles) + ); + } + + public toJSON(): DistinguishingStyles.JSON { + return { + ...super.toJSON(), + defaultStyle: this._defaultStyles.toJSON(), + hoverStyle: this._hoverStyles.toJSON(), + focusStyle: this._focusStyles.toJSON(), + }; + } +} + +/** + * @internal + */ +export namespace DistinguishingStyles { + export interface JSON extends Diagnostic.JSON { + defaultStyle: Sequence.JSON>; + hoverStyle: Sequence.JSON>; + focusStyle: Sequence.JSON>; + } + + export function isDistinguishingStyles( + value: Diagnostic + ): value is DistinguishingStyles; + + export function isDistinguishingStyles( + value: unknown + ): value is DistinguishingStyles; + + export function isDistinguishingStyles( + value: unknown + ): value is DistinguishingStyles { + return value instanceof DistinguishingStyles; + } +} diff --git a/packages/alfa-rules/src/sia-er62/serialise.ts b/packages/alfa-rules/src/sia-dr62/serialise.ts similarity index 89% rename from packages/alfa-rules/src/sia-er62/serialise.ts rename to packages/alfa-rules/src/sia-dr62/serialise.ts index 4751fd6f60..66a1744511 100644 --- a/packages/alfa-rules/src/sia-er62/serialise.ts +++ b/packages/alfa-rules/src/sia-dr62/serialise.ts @@ -98,34 +98,6 @@ export namespace Serialise { return serializedShadows.join(", "); } - export function font(style: Style): string { - const optional = (["style", "weight"] as const) - .map((property) => getLonghand(style, `font-${property}`)) - .join(" "); - - const size = style.computed("font-size"); - const family = style.computed("font-family"); - - if (optional !== " ") { - // Optional properties were changed, need to output the mandatory ones. - return normalize(`${optional} ${size} ${family}`); - } - - if ( - size.value.equals(Property.get("font-size").initial.value) && - family.value.values[0].equals( - Property.get("font-family").initial.values[0] - ) - ) { - // Both mandatory properties are set to their initial values. - // Since optional properties also are, we can skip `font` altogether. - return ""; - } - - // Optional properties were not changed but some mandatory ones were. - return normalize(`${size} ${family}`); - } - // Only background-color and background-image are used for deciding if the // link is distinguishable, but all longhands are needed for rendering it // with the correct style. diff --git a/packages/alfa-rules/src/sia-er62/rule.ts b/packages/alfa-rules/src/sia-er62/rule.ts deleted file mode 100644 index f501d8ea81..0000000000 --- a/packages/alfa-rules/src/sia-er62/rule.ts +++ /dev/null @@ -1,660 +0,0 @@ -import { Rule } from "@siteimprove/alfa-act"; -import { DOM } from "@siteimprove/alfa-aria"; -import { Array } from "@siteimprove/alfa-array"; -import { Cache } from "@siteimprove/alfa-cache"; -import { Color, Keyword } from "@siteimprove/alfa-css"; -import { Device } from "@siteimprove/alfa-device"; -import { Element, Node, Text } from "@siteimprove/alfa-dom"; -import { Iterable } from "@siteimprove/alfa-iterable"; -import { List } from "@siteimprove/alfa-list"; -import { Map } from "@siteimprove/alfa-map"; -import { Option, None } from "@siteimprove/alfa-option"; -import { Predicate } from "@siteimprove/alfa-predicate"; -import { Refinement } from "@siteimprove/alfa-refinement"; -import { Err, Ok, Result } from "@siteimprove/alfa-result"; -import { Context } from "@siteimprove/alfa-selector"; -import { Sequence } from "@siteimprove/alfa-sequence"; -import { Set } from "@siteimprove/alfa-set"; -import { Style } from "@siteimprove/alfa-style"; -import { Criterion } from "@siteimprove/alfa-wcag"; -import { Page } from "@siteimprove/alfa-web"; - -import { expectation } from "../common/act/expectation"; - -import { Contrast } from "../common/diagnostic/contrast"; -import { contrast } from "../common/expectation/contrast"; - -import { getForeground } from "../common/dom/get-colors"; -import { isWhitespace } from "../common/predicate"; - -import { Scope, Stability, Version } from "../tags"; - -import { - DistinguishingStyles, - ElementDistinguishable, - DistinguishingProperty, -} from "./diagnostics"; - -const { hasRole } = DOM; -const { isElement } = Element; -const { or, not, test, tee } = Predicate; -const { and } = Refinement; -const { - hasBorder, - hasBoxShadow, - hasComputedStyle, - hasOutline, - hasTextDecoration, - isVisible, -} = Style; -const { isText } = Text; - -let distinguishingProperties: Map< - Context, - Map> -> = Map.empty(); - -/** - * This version of R62 accepts differences in `font-family`, differences - * in `cursor` (in the `hover` state), and 3:1 or more contrast with surrounding - * text. - * - * These cannot be easily displayed in the current Page report: - * * `cursor` only shows on hovering, we could have a `a:link` and `a:hover` - * states that look similar, but the first one is bad and the second good, - * resulting in confusing info; - * * contrast with surrounding text is not shown as only link text is shown in - * the decorator. Additionally, we could have similar looking states but with - * different colors, one being good and the other bad, with little way to - * understand the difference. - * * `font-family` may depend on a font that for some reason can't be loaded, - * so the difference wouldn't be visible. - */ -export default Rule.Atomic.of({ - uri: "https://alfa.siteimprove.com/rules/sia-r62", - requirements: [Criterion.of("1.4.1")], - tags: [Scope.Component, Stability.Experimental, Version.of(2)], - evaluate({ device, document }) { - // Contains links (key) and their containing paragraph (value) - let containers: Map = Map.empty(); - - return { - applicability() { - // Contains links (key) and the parents of the textnodes the links contain (value) - let linkText: Map> = Map.empty(); - // Contains containers (key) and the parents of the text nodes - // (not included in links) the containers have (value) - let nonLinkText: Map> = Map.empty(); - - gather(document, None, None); - return getApplicableLinks(); - - function gather( - node: Node, - container: Option, - link: Option - ): void { - const isLink = hasRole(device, (role) => role.is("link")); - const isParagraph = hasRole(device, "paragraph"); - - if (isElement(node)) { - if (container.isSome() && isLink(node)) { - // For each link, store its containing paragraph - containers = containers.set(node, container.get()); - link = Option.of(node); - } - - // Otherwise, if the element is a paragraph element with non-link text - // content then start collecting applicable elements. - if (isParagraph(node)) { - if (test(hasNonLinkText(device), node)) { - // Start gathering links inside a paragraph with non-link text. - container = Option.of(node); - } else { - // Stop gathering inside a paragraph without non-link text. - container = None; - } - } - } else { - const isTextNode = test(and(isText, isVisible(device)), node); - const parent = node.parent().filter(isElement); - - if (isTextNode && container.isSome() && parent.isSome()) { - // For each link, store the parent of the text nodes it contains - if (link.isSome()) { - linkText = linkText.set( - link.get(), - linkText - .get(link.get()) - .getOr(Set.empty()) - .add(parent.get()) - ); - } - // For each container, store the parent of the text nodes it contains - else { - nonLinkText = nonLinkText.set( - container.get(), - nonLinkText - .get(container.get()) - .getOr(Set.empty()) - .add(parent.get()) - ); - } - } - } - - for (const child of node.children(Node.fullTree)) { - gather(child, container, link); - } - } - - function* getApplicableLinks(): Iterable { - // Check if foreground is the same with the parent

element - const hasDifferentForeground = ( - link: Element, - container: Element - ): boolean => - getForeground(link, device).none( - (linkColors) => - // If the link has a foreground with the alpha channel less than 1 and background gradient color - // then the rule is applicable as we can't tell for sure if it ever has the same foreground with a link - // that might have the same foreground and gradient background, but with different gradient type or spread - linkColors.length === 1 && - getForeground(container, device).some( - (containerColors) => - // Similalry to the link, we assume the rule is applicable if the container has more than one foreground color - containerColors.length === 1 && - linkColors[0].equals(containerColors[0]) - ) - ); - - for (const [link, linkTexts] of linkText) { - const nonLinkTexts = nonLinkText - .get(containers.get(link).get()) - // At this point, we should always have something, still - // safeguarding against any weird case. - .getOr(Set.empty()); - - if ( - linkTexts.some((linkElement) => - nonLinkTexts.some((nonLinkElement) => - hasDifferentForeground(linkElement, nonLinkElement) - ) - ) - ) { - yield link; - } - } - } - }, - - expectations(target) { - const nonLinkElements = containers - .get(target) - .get() - .inclusiveDescendants(Node.fullTree) - .filter(and(isElement, hasNonLinkText(device))); - - const linkElements = target - // All descendants of the link. - .inclusiveDescendants(Node.fullTree) - .filter(isElement) - // Plus those ancestors who don't include non-link text. - .concat( - target - .ancestors(Node.fullTree) - .takeWhile(and(isElement, not(hasNonLinkText(device)))) - ); - - const hasDistinguishingStyle = (context: Context = Context.empty()) => - Set.from( - linkElements.map((link) => { - // If the link element is distinguishable from at least one - // non-link element, this is good enough. - // Note that ACT rules draft requires the link-element to be - // distinguishable from *all* non-link elements in order to be good. - const hasDistinguishableStyle = nonLinkElements.some( - (container) => - Distinguishable.isDistinguishable( - container, - target, - device, - context - ) - .map((isDistinguishable) => isDistinguishable(link)) - .some((distinguishable) => distinguishable) - ); - - const distinguishableContrast = Set.from( - nonLinkElements.flatMap((container) => - Sequence.from( - Distinguishable.getPairwiseContrast( - container, - link, - device, - context - ) - ) - ) - ); - - const properties: List = - distinguishingProperties - .get(context) - .flatMap((elementMap) => elementMap.get(link)) - .getOrElse(() => List.empty()); - - return hasDistinguishableStyle - ? Ok.of( - ElementDistinguishable.from( - link, - device, - target, - context, - properties, - distinguishableContrast - ) - ) - : Err.of( - ElementDistinguishable.from( - link, - device, - target, - context, - properties, - distinguishableContrast - ) - ); - }) - ) - .toArray() - // sort the Ok before the Err, relative order doesn't matter. - .sort((a, b) => (b.isOk() ? 1 : -1)); - - // The context needs to be set on the *target*, not on its ancestors - // or descendants - const isDefaultDistinguishable = hasDistinguishingStyle(); - - const isHoverDistinguishable = hasDistinguishingStyle( - Context.hover(target) - ); - - const isFocusDistinguishable = hasDistinguishingStyle( - Context.focus(target) - ); - - return { - 1: expectation( - // If at least one link element is good, this is enough. The sorting - // guarantees it is first in the array. - isDefaultDistinguishable[0].isOk() && - isHoverDistinguishable[0].isOk() && - isFocusDistinguishable[0].isOk(), - () => - Outcomes.IsDistinguishable( - isDefaultDistinguishable, - isHoverDistinguishable, - isFocusDistinguishable - ), - () => - Outcomes.IsNotDistinguishable( - isDefaultDistinguishable, - isHoverDistinguishable, - isFocusDistinguishable - ) - ), - }; - }, - }; - }, -}); - -export namespace Outcomes { - // We could tweak typing to ensure that isDistinguishable only accepts Ok and - // that isNotDistinguishable has at least one Err. - // This would requires changing the expectation since it does not refine - // and is thus probably not worth the effort. - export const IsDistinguishable = ( - defaultStyles: Iterable>, - hoverStyles: Iterable>, - focusStyles: Iterable> - ) => - Ok.of( - DistinguishingStyles.of( - `The link is distinguishable from the surrounding text`, - defaultStyles, - hoverStyles, - focusStyles - ) - ); - - export const IsNotDistinguishable = ( - defaultStyles: Iterable>, - hoverStyles: Iterable>, - focusStyles: Iterable> - ) => - Err.of( - DistinguishingStyles.of( - `The link is not distinguishable from the surrounding text`, - defaultStyles, - hoverStyles, - focusStyles - ) - ); -} - -const hasNonLinkTextCache = Cache.empty(); - -function hasNonLinkText(device: Device): Predicate { - return function hasNonLinkText(element) { - return hasNonLinkTextCache.get(element, () => { - // If we are already below a link, escape. - if ( - element.inclusiveAncestors(Node.flatTree).some( - and( - isElement, - hasRole(device, (role) => role.is("link")) - ) - ) - ) { - return false; - } - - const children = element.children(Node.flatTree); - - // If we've found text with more than whitespaces, we're done. - if ( - children.some( - and( - isText, - and(isVisible(device), (text) => !isWhitespace(text.data)) - ) - ) - ) { - return true; - } - - // Otherwise, go down. - return ( - children - .filter(isElement) - .reject(hasRole(device, (role) => role.is("link"))) - // We've found nested paragraphs. While this is not really allowed by - // HTML specs, it does happen… - // In such a case, the inner paragraph would itself be a potential container - // and any text in it should be registered with it, not with the outer one - .reject(hasRole(device, "paragraph")) - .some(hasNonLinkText) - ); - }); - }; -} - -namespace Distinguishable { - export function isDistinguishable( - container: Element, - target: Element, - device: Device, - context: Context = Context.empty() - ): Array> { - let predicates: Array< - readonly [DistinguishingProperty, Predicate] - > = [ - // Things like text decoration and backgrounds risk blending with the - // container element. We therefore need to check if these can be distinguished - // from what the container element might itself set. - ["background", hasDistinguishableBackground(container, device, context)], - ["contrast", hasDistinguishableContrast(container, device, context)], - ["font", hasDistinguishableFont(container, device, context)], - [ - "text-decoration", - hasDistinguishableTextDecoration(container, device, context), - ], - [ - "vertical-align", - hasDistinguishableVerticalAlign(container, device, context), - ], - // We consider the mere presence of borders, box-shadows or outlines on the element as - // distinguishable features. There's of course a risk of these blending with - // other features of the container element, such as its background, but this - // should hopefully not happen (too often) in practice. When it does, we - // risk false negatives. - ["border", hasBorder(device, context)], - [ - "box-shadow", - hasBoxShadow(device, context), //Checks for color != transparent and spread => 0 - ], - ["outline", hasOutline(device, context)], - ]; - - if (context.isHovered(target)) { - predicates = [ - ...predicates, - ["cursor", hasDistinguishableCursor(container, device, context)], - ]; - } - - return predicates.map(([name, predicate]) => - tee(predicate, (link, result) => { - if (result) { - let linkToProperties = distinguishingProperties - .get(context) - .getOr(Map.empty>()); - - const properties = linkToProperties - .get(link) - .getOr(List.empty()) - .append(name); - - distinguishingProperties = distinguishingProperties.set( - context, - linkToProperties.set(link, properties) - ); - } - }) - ); - } - - function hasDistinguishableTextDecoration( - container: Element, - device: Device, - context?: Context - ): Predicate { - return (element) => - test(not(hasTextDecoration(device, context)), container) && - test(hasTextDecoration(device, context), element); - } - - /** - * Check if an element has a distinguishable background from the given container - * element. - * - * @remarks - * This predicate currently only considers `background-color` and - * `background-image` as a possibly distinguishable background. Other - * `background-*` properties should ideally also be considered. - * - * Additionally, this predicate do not handle transparency in the topmost layer. - * The exact same (partly transparent) `background-color` or `background-image` - * could be on top of a different (opaque) background and thus creates a - * difference. However, in these cases the (lower layer) distinguishing - * background would be on an ancestor of the link but of no non-link text (in - * order to be distinguishing), so should be found when looking at the ancestors - * of the link. - * - * Lastly, this does not account for absolutely positioned backgrounds from - * random nodes in the DOM. Using these to push an image below links in - * paragraph sounds so crazy (from a sheer code maintenance point of view) that - * this hopefully won't be a problem. - */ - function hasDistinguishableBackground( - container: Element, - device: Device, - context?: Context - ): Predicate { - const colorReference = Style.from(container, device, context).computed( - "background-color" - ).value; - - const imageReference = Style.from(container, device, context).computed( - "background-image" - ).value; - - return (link: Element) => { - const color = Style.from(link, device, context).computed( - "background-color" - ).value; - - const image = Style.from(link, device, context).computed( - "background-image" - ).value; - - // If the background is fully transparent or there is no `background-image` set on the link, - // we assume it will end up being the same as the container. Intermediate backgrounds may change - // that, but these would need to be set on ancestor of the link and of - // no non-link text, so will be caught in one of the other comparisons. - const hasBackground = !( - Keyword.isKeyword(image.values[0]) && - image.values[0].equals(Keyword.of("none")) && - Color.isTransparent(color) - ); - - // Any difference in `background-image` is considered enough. If different - // `background-image` ultimately yield the same background (e.g. the same - // image at two different URLs), this creates false negatives. - // When there is no `background-image` set on the link, we consider it to be the same as the container's - const hasDifferentBackgroundFromContainer = !( - color.equals(colorReference) && image.equals(imageReference) - ); - - return hasDifferentBackgroundFromContainer && hasBackground; - }; - } - - export function getPairwiseContrast( - container: Element, - link: Element, - device: Device, - context: Context = Context.empty() - ): ReadonlyArray> { - return getForeground(container, device, context) - .map((containerColors) => [ - ...Array.flatMap(containerColors, (containerColor) => - getForeground(link, device, context) - .map((linkColors) => - Array.map(linkColors, (linkColor) => - Contrast.Pairing.of<["container", "link"]>( - ["container", containerColor], - ["link", linkColor], - contrast(containerColor, linkColor) - ) - ) - ) - .getOr([]) - ), - ]) - .getOr([]); - } - - function hasDistinguishableContrast( - container: Element, - device: Device, - context: Context = Context.empty() - ): Predicate { - return (link) => { - for (const contrastPairing of getPairwiseContrast( - container, - link, - device, - context - )) { - // If at least one of the contrast values are bigger than the threshold, the link is marked distinguisable - if (contrastPairing.contrast >= 3) { - return true; - } - } - return false; - }; - } - - /** - * Check if an element has a different font weight or family than its container. - * - * This is brittle and imperfect but removes a strong pain point until we find - * a better solution. - */ - - function hasDistinguishableFont( - container: Element, - device: Device, - context?: Context - ): Predicate { - const style = Style.from(container, device, context); - - const referenceWeight = style.computed("font-weight").value; - const referenceFamily = Option.from( - style.computed("font-family").value.values[0] - ); - - return or( - hasComputedStyle( - "font-weight", - not((weight) => weight.equals(referenceWeight)), - device, - context - ), - hasComputedStyle( - "font-family", - not((family) => Option.from(family.values[0]).equals(referenceFamily)), - device, - context - ) - ); - } - - function hasDistinguishableVerticalAlign( - container: Element, - device: Device, - context?: Context - ): Predicate { - const reference = Style.from(container, device, context).computed( - "vertical-align" - ).value; - - return hasComputedStyle( - "vertical-align", - not((alignment) => alignment.equals(reference)), - device, - context - ); - } - - function hasDistinguishableCursor( - container: Element, - device: Device, - context?: Context - ): Predicate { - // Checking if there is a custom cursor, otherwise grabbing the built-in - function getFirstCursor(style: Style.Computed<"cursor">) { - return style.values[0].values.length !== 0 - ? style.values[0].values[0] - : style.values[1]; - } - const containerCursorStyle = Style.from( - container, - device, - context - ).computed("cursor").value; - - // We assume that the first custom cursor, if any, will never fail to load - // and thus don't try to default further. - const reference = getFirstCursor(containerCursorStyle); - - return hasComputedStyle( - "cursor", - not((cursor) => getFirstCursor(cursor).equals(reference)), - device, - context - ); - } -} diff --git a/packages/alfa-rules/src/sia-er62/diagnostics.ts b/packages/alfa-rules/src/sia-r62/diagnostics.ts similarity index 100% rename from packages/alfa-rules/src/sia-er62/diagnostics.ts rename to packages/alfa-rules/src/sia-r62/diagnostics.ts diff --git a/packages/alfa-rules/src/sia-r62/rule.ts b/packages/alfa-rules/src/sia-r62/rule.ts index df8ec243c2..4e2a3b64dc 100644 --- a/packages/alfa-rules/src/sia-r62/rule.ts +++ b/packages/alfa-rules/src/sia-r62/rule.ts @@ -1,12 +1,12 @@ -import { Rule, Diagnostic } from "@siteimprove/alfa-act"; +import { Rule } from "@siteimprove/alfa-act"; import { DOM } from "@siteimprove/alfa-aria"; +import { Array } from "@siteimprove/alfa-array"; import { Cache } from "@siteimprove/alfa-cache"; -import { Color } from "@siteimprove/alfa-css"; +import { Color, Keyword } from "@siteimprove/alfa-css"; import { Device } from "@siteimprove/alfa-device"; import { Element, Node, Text } from "@siteimprove/alfa-dom"; -import { Equatable } from "@siteimprove/alfa-equatable"; -import { Hash, Hashable } from "@siteimprove/alfa-hash"; -import { Serializable } from "@siteimprove/alfa-json"; +import { Iterable } from "@siteimprove/alfa-iterable"; +import { List } from "@siteimprove/alfa-list"; import { Map } from "@siteimprove/alfa-map"; import { Option, None } from "@siteimprove/alfa-option"; import { Predicate } from "@siteimprove/alfa-predicate"; @@ -15,21 +15,29 @@ import { Err, Ok, Result } from "@siteimprove/alfa-result"; import { Context } from "@siteimprove/alfa-selector"; import { Sequence } from "@siteimprove/alfa-sequence"; import { Set } from "@siteimprove/alfa-set"; -import { Property, Style } from "@siteimprove/alfa-style"; +import { Style } from "@siteimprove/alfa-style"; import { Criterion } from "@siteimprove/alfa-wcag"; import { Page } from "@siteimprove/alfa-web"; -import * as json from "@siteimprove/alfa-json"; - import { expectation } from "../common/act/expectation"; + +import { Contrast } from "../common/diagnostic/contrast"; +import { contrast } from "../common/expectation/contrast"; + +import { getForeground } from "../common/dom/get-colors"; import { isWhitespace } from "../common/predicate"; -import { Scope } from "../tags"; -import { Serialise } from "./serialise"; +import { Scope, Version } from "../tags"; + +import { + DistinguishingStyles, + ElementDistinguishable, + DistinguishingProperty, +} from "./diagnostics"; const { hasRole } = DOM; const { isElement } = Element; -const { or, not, test } = Predicate; +const { or, not, test, tee } = Predicate; const { and } = Refinement; const { hasBorder, @@ -41,54 +49,141 @@ const { } = Style; const { isText } = Text; +let distinguishingProperties: Map< + Context, + Map> +> = Map.empty(); + +/** + * This version of R62 accepts differences in `font-family`, differences + * in `cursor` (in the `hover` state), and 3:1 or more contrast with surrounding + * text. + * + * These cannot be easily displayed in the current Page report: + * * `cursor` only shows on hovering, we could have a `a:link` and `a:hover` + * states that look similar, but the first one is bad and the second good, + * resulting in confusing info; + * * contrast with surrounding text is not shown as only link text is shown in + * the decorator. Additionally, we could have similar looking states but with + * different colors, one being good and the other bad, with little way to + * understand the difference. + * * `font-family` may depend on a font that for some reason can't be loaded, + * so the difference wouldn't be visible. + */ export default Rule.Atomic.of({ uri: "https://alfa.siteimprove.com/rules/sia-r62", requirements: [Criterion.of("1.4.1")], - tags: [Scope.Component], + tags: [Scope.Component, Version.of(2)], evaluate({ device, document }) { + // Contains links (key) and their containing paragraph (value) let containers: Map = Map.empty(); return { applicability() { - return visit(document, None); + // Contains links (key) and the parents of the textnodes the links contain (value) + let linkText: Map> = Map.empty(); + // Contains containers (key) and the parents of the text nodes + // (not included in links) the containers have (value) + let nonLinkText: Map> = Map.empty(); - function* visit( + gather(document, None, None); + return getApplicableLinks(); + + function gather( node: Node, - container: Option - ): Iterable { + container: Option, + link: Option + ): void { + const isLink = hasRole(device, (role) => role.is("link")); + const isParagraph = hasRole(device, "paragraph"); + if (isElement(node)) { - // If the element is a semantic link, it might be applicable. - if ( - test( - hasRole(device, (role) => role.is("link")), - node - ) - ) { - if ( - container.isSome() && - node - .descendants(Node.flatTree) - .some(and(isText, isVisible(device))) - ) { - containers = containers.set(node, container.get()); - return yield node; - } + if (container.isSome() && isLink(node)) { + // For each link, store its containing paragraph + containers = containers.set(node, container.get()); + link = Option.of(node); } - // Otherwise, if the element is a

element with non-link text + // Otherwise, if the element is a paragraph element with non-link text // content then start collecting applicable elements. - else if ( - test( - and(hasRole(device, "paragraph"), hasNonLinkText(device)), - node - ) - ) { - container = Option.of(node); + if (isParagraph(node)) { + if (test(hasNonLinkText(device), node)) { + // Start gathering links inside a paragraph with non-link text. + container = Option.of(node); + } else { + // Stop gathering inside a paragraph without non-link text. + container = None; + } + } + } else { + const isTextNode = test(and(isText, isVisible(device)), node); + const parent = node.parent().filter(isElement); + + if (isTextNode && container.isSome() && parent.isSome()) { + // For each link, store the parent of the text nodes it contains + if (link.isSome()) { + linkText = linkText.set( + link.get(), + linkText + .get(link.get()) + .getOr(Set.empty()) + .add(parent.get()) + ); + } + // For each container, store the parent of the text nodes it contains + else { + nonLinkText = nonLinkText.set( + container.get(), + nonLinkText + .get(container.get()) + .getOr(Set.empty()) + .add(parent.get()) + ); + } } } for (const child of node.children(Node.fullTree)) { - yield* visit(child, container); + gather(child, container, link); + } + } + + function* getApplicableLinks(): Iterable { + // Check if foreground is the same with the parent

element + const hasDifferentForeground = ( + link: Element, + container: Element + ): boolean => + getForeground(link, device).none( + (linkColors) => + // If the link has a foreground with the alpha channel less than 1 and background gradient color + // then the rule is applicable as we can't tell for sure if it ever has the same foreground with a link + // that might have the same foreground and gradient background, but with different gradient type or spread + linkColors.length === 1 && + getForeground(container, device).some( + (containerColors) => + // Similalry to the link, we assume the rule is applicable if the container has more than one foreground color + containerColors.length === 1 && + linkColors[0].equals(containerColors[0]) + ) + ); + + for (const [link, linkTexts] of linkText) { + const nonLinkTexts = nonLinkText + .get(containers.get(link).get()) + // At this point, we should always have something, still + // safeguarding against any weird case. + .getOr(Set.empty()); + + if ( + linkTexts.some((linkElement) => + nonLinkTexts.some((nonLinkElement) => + hasDifferentForeground(linkElement, nonLinkElement) + ) + ) + ) { + yield link; + } } } }, @@ -111,19 +206,66 @@ export default Rule.Atomic.of({ .takeWhile(and(isElement, not(hasNonLinkText(device)))) ); - const hasDistinguishingStyle = (context?: Context) => + const hasDistinguishingStyle = (context: Context = Context.empty()) => Set.from( - linkElements.map((link) => + linkElements.map((link) => { // If the link element is distinguishable from at least one // non-link element, this is good enough. // Note that ACT rules draft requires the link-element to be // distinguishable from *all* non-link elements in order to be good. - nonLinkElements.some((container) => - isDistinguishable(container, device, context)(link) - ) - ? Ok.of(ComputedStyles.from(link, device, context)) - : Err.of(ComputedStyles.from(link, device, context)) - ) + const hasDistinguishableStyle = nonLinkElements.some( + (container) => + Distinguishable.isDistinguishable( + container, + target, + device, + context + ) + .map((isDistinguishable) => isDistinguishable(link)) + .some((distinguishable) => distinguishable) + ); + + const distinguishableContrast = Set.from( + nonLinkElements.flatMap((container) => + Sequence.from( + Distinguishable.getPairwiseContrast( + container, + link, + device, + context + ) + ) + ) + ); + + const properties: List = + distinguishingProperties + .get(context) + .flatMap((elementMap) => elementMap.get(link)) + .getOrElse(() => List.empty()); + + return hasDistinguishableStyle + ? Ok.of( + ElementDistinguishable.from( + link, + device, + target, + context, + properties, + distinguishableContrast + ) + ) + : Err.of( + ElementDistinguishable.from( + link, + device, + target, + context, + properties, + distinguishableContrast + ) + ); + }) ) .toArray() // sort the Ok before the Err, relative order doesn't matter. @@ -173,9 +315,9 @@ export namespace Outcomes { // This would requires changing the expectation since it does not refine // and is thus probably not worth the effort. export const IsDistinguishable = ( - defaultStyles: Iterable>, - hoverStyles: Iterable>, - focusStyles: Iterable> + defaultStyles: Iterable>, + hoverStyles: Iterable>, + focusStyles: Iterable> ) => Ok.of( DistinguishingStyles.of( @@ -187,9 +329,9 @@ export namespace Outcomes { ); export const IsNotDistinguishable = ( - defaultStyles: Iterable>, - hoverStyles: Iterable>, - focusStyles: Iterable> + defaultStyles: Iterable>, + hoverStyles: Iterable>, + focusStyles: Iterable> ) => Err.of( DistinguishingStyles.of( @@ -233,316 +375,286 @@ function hasNonLinkText(device: Device): Predicate { } // Otherwise, go down. - return children - .filter(isElement) - .reject(hasRole(device, (role) => role.is("link"))) - .some(hasNonLinkText); + return ( + children + .filter(isElement) + .reject(hasRole(device, (role) => role.is("link"))) + // We've found nested paragraphs. While this is not really allowed by + // HTML specs, it does happen… + // In such a case, the inner paragraph would itself be a potential container + // and any text in it should be registered with it, not with the outer one + .reject(hasRole(device, "paragraph")) + .some(hasNonLinkText) + ); }); }; } -function isDistinguishable( - container: Element, - device: Device, - context: Context = Context.empty() -): Predicate { - return or( - // Things like text decoration and backgrounds risk blending with the - // container element. We therefore need to check if these can be distinguished - // from what the container element might itself set. - hasDistinguishableTextDecoration(container, device, context), - hasDistinguishableBackground(container, device, context), - - hasDistinguishableFontWeight(container, device, context), - hasDistinguishableVerticalAlign(container, device, context), - // We consider the mere presence of borders or outlines on the element as - // distinguishable features. There's of course a risk of these blending with - // other features of the container element, such as its background, but this - // should hopefully not happen (too often) in practice. When it does, we - // risk false negatives. - hasOutline(device, context), - hasBorder(device, context), - hasBoxShadow(device, context) //Checks for color != transparent and spread => 0 - ); -} - -function hasDistinguishableTextDecoration( - container: Element, - device: Device, - context?: Context -): Predicate { - return (element) => - test(not(hasTextDecoration(device, context)), container) && - test(hasTextDecoration(device, context), element); -} - -/** - * Check if an element has a distinguishable background from the given container - * element. - * - * @remarks - * This predicate currently only considers `background-color` and - * `background-image` as a possibly distinguishable background. Other - * `background-*` properties should ideally also be considered. - * - * Additionally, this predicate do not handle transparency in the topmost layer. - * The exact same (partly transparent) `background-color` or `background-image` - * could be on top of a different (opaque) background and thus creates a - * difference. However, in these cases the (lower layer) distinguishing - * background would be on an ancestor of the link but of no non-link text (in - * order to be distinguishing), so should be found when looking at the ancestors - * of the link. - * - * Lastly, this does not account for absolutely positioned backgrounds from - * random nodes in the DOM. Using these to push an image below links in - * paragraph sounds so crazy (from a sheer code maintenance point of view) that - * this hopefully won't be a problem. - */ -function hasDistinguishableBackground( - container: Element, - device: Device, - context?: Context -): Predicate { - const colorReference = Style.from(container, device, context).computed( - "background-color" - ).value; - - const imageReference = Style.from(container, device, context).computed( - "background-image" - ).value; - - return or( - hasComputedStyle( - "background-color", - not( - // If the background is fully transparent, we assume it will end up - // being the same as the container. Intermediate backgrounds may change - // that, but these would need to be set on ancestor of the link and of - // no non-link text, so will be caught in one of the other comparisons. - (color) => Color.isTransparent(color) || color.equals(colorReference) - ), - device, - context - ), - // Any difference in `background-image` is considered enough. If different - // `background-image` ultimately yield the same background (e.g. the same - // image at two different URLs), this creates false negatives. - hasComputedStyle( - "background-image", - not((image) => image.equals(imageReference)), - device, - context - ) - ); -} - -/** - * Check if an element has a different font weight than its container. - * - * This is brittle and imperfect but removes a strong pain point until we find - * a better solution. - */ -function hasDistinguishableFontWeight( - container: Element, - device: Device, - context?: Context -): Predicate { - const reference = Style.from(container, device, context).computed( - "font-weight" - ).value; - - return hasComputedStyle( - "font-weight", - not((weight) => weight.equals(reference)), - device, - context - ); -} - -function hasDistinguishableVerticalAlign( - container: Element, - device: Device, - context?: Context -): Predicate { - const reference = Style.from(container, device, context).computed( - "vertical-align" - ).value; - - return hasComputedStyle( - "vertical-align", - not((alignment) => alignment.equals(reference)), - device, - context - ); -} - -type Name = Property.Name | Property.Shorthand.Name; - -export class ComputedStyles implements Equatable, Hashable, Serializable { - public static of( - style: Iterable = [] - ): ComputedStyles { - return new ComputedStyles(Map.from(style)); - } - - private readonly _style: Map; - - private constructor(style: Map) { - this._style = style; +namespace Distinguishable { + export function isDistinguishable( + container: Element, + target: Element, + device: Device, + context: Context = Context.empty() + ): Array> { + let predicates: Array< + readonly [DistinguishingProperty, Predicate] + > = [ + // Things like text decoration and backgrounds risk blending with the + // container element. We therefore need to check if these can be distinguished + // from what the container element might itself set. + ["background", hasDistinguishableBackground(container, device, context)], + ["contrast", hasDistinguishableContrast(container, device, context)], + ["font", hasDistinguishableFont(container, device, context)], + [ + "text-decoration", + hasDistinguishableTextDecoration(container, device, context), + ], + [ + "vertical-align", + hasDistinguishableVerticalAlign(container, device, context), + ], + // We consider the mere presence of borders, box-shadows or outlines on the element as + // distinguishable features. There's of course a risk of these blending with + // other features of the container element, such as its background, but this + // should hopefully not happen (too often) in practice. When it does, we + // risk false negatives. + ["border", hasBorder(device, context)], + [ + "box-shadow", + hasBoxShadow(device, context), //Checks for color != transparent and spread => 0 + ], + ["outline", hasOutline(device, context)], + ]; + + if (context.isHovered(target)) { + predicates = [ + ...predicates, + ["cursor", hasDistinguishableCursor(container, device, context)], + ]; + } + + return predicates.map(([name, predicate]) => + tee(predicate, (link, result) => { + if (result) { + let linkToProperties = distinguishingProperties + .get(context) + .getOr(Map.empty>()); + + const properties = linkToProperties + .get(link) + .getOr(List.empty()) + .append(name); + + distinguishingProperties = distinguishingProperties.set( + context, + linkToProperties.set(link, properties) + ); + } + }) + ); } - public get style(): Map { - return this._style; + function hasDistinguishableTextDecoration( + container: Element, + device: Device, + context?: Context + ): Predicate { + return (element) => + test(not(hasTextDecoration(device, context)), container) && + test(hasTextDecoration(device, context), element); } - public equals(value: ComputedStyles): boolean; - - public equals(value: unknown): value is this; - - public equals(value: unknown): boolean { - return value instanceof ComputedStyles && value._style.equals(this._style); + /** + * Check if an element has a distinguishable background from the given container + * element. + * + * @remarks + * This predicate currently only considers `background-color` and + * `background-image` as a possibly distinguishable background. Other + * `background-*` properties should ideally also be considered. + * + * Additionally, this predicate do not handle transparency in the topmost layer. + * The exact same (partly transparent) `background-color` or `background-image` + * could be on top of a different (opaque) background and thus creates a + * difference. However, in these cases the (lower layer) distinguishing + * background would be on an ancestor of the link but of no non-link text (in + * order to be distinguishing), so should be found when looking at the ancestors + * of the link. + * + * Lastly, this does not account for absolutely positioned backgrounds from + * random nodes in the DOM. Using these to push an image below links in + * paragraph sounds so crazy (from a sheer code maintenance point of view) that + * this hopefully won't be a problem. + */ + function hasDistinguishableBackground( + container: Element, + device: Device, + context?: Context + ): Predicate { + const colorReference = Style.from(container, device, context).computed( + "background-color" + ).value; + + const imageReference = Style.from(container, device, context).computed( + "background-image" + ).value; + + return (link: Element) => { + const color = Style.from(link, device, context).computed( + "background-color" + ).value; + + const image = Style.from(link, device, context).computed( + "background-image" + ).value; + + // If the background is fully transparent or there is no `background-image` set on the link, + // we assume it will end up being the same as the container. Intermediate backgrounds may change + // that, but these would need to be set on ancestor of the link and of + // no non-link text, so will be caught in one of the other comparisons. + const hasBackground = !( + Keyword.isKeyword(image.values[0]) && + image.values[0].equals(Keyword.of("none")) && + Color.isTransparent(color) + ); + + // Any difference in `background-image` is considered enough. If different + // `background-image` ultimately yield the same background (e.g. the same + // image at two different URLs), this creates false negatives. + // When there is no `background-image` set on the link, we consider it to be the same as the container's + const hasDifferentBackgroundFromContainer = !( + color.equals(colorReference) && image.equals(imageReference) + ); + + return hasDifferentBackgroundFromContainer && hasBackground; + }; } - public hash(hash: Hash): void { - this._style.hash(hash); + export function getPairwiseContrast( + container: Element, + link: Element, + device: Device, + context: Context = Context.empty() + ): ReadonlyArray> { + return getForeground(container, device, context) + .map((containerColors) => [ + ...Array.flatMap(containerColors, (containerColor) => + getForeground(link, device, context) + .map((linkColors) => + Array.map(linkColors, (linkColor) => + Contrast.Pairing.of<["container", "link"]>( + ["container", containerColor], + ["link", linkColor], + contrast(containerColor, linkColor) + ) + ) + ) + .getOr([]) + ), + ]) + .getOr([]); } - public toJSON(): ComputedStyles.JSON { - return { - style: this._style.toJSON(), + function hasDistinguishableContrast( + container: Element, + device: Device, + context: Context = Context.empty() + ): Predicate { + return (link) => { + for (const contrastPairing of getPairwiseContrast( + container, + link, + device, + context + )) { + // If at least one of the contrast values are bigger than the threshold, the link is marked distinguisable + if (contrastPairing.contrast >= 3) { + return true; + } + } + return false; }; } -} -export namespace ComputedStyles { - export interface JSON { - [key: string]: json.JSON; - style: Map.JSON; - } + /** + * Check if an element has a different font weight or family than its container. + * + * This is brittle and imperfect but removes a strong pain point until we find + * a better solution. + */ - export function isComputedStyles(value: unknown): value is ComputedStyles { - return value instanceof ComputedStyles; - } - - export function from( - element: Element, + function hasDistinguishableFont( + container: Element, device: Device, - context: Context = Context.empty() - ): ComputedStyles { - const style = Style.from(element, device, context); + context?: Context + ): Predicate { + const style = Style.from(container, device, context); - const border = (["color", "style", "width"] as const).map((property) => - Serialise.borderShorthand(style, property) + const referenceWeight = style.computed("font-weight").value; + const referenceFamily = Option.from( + style.computed("font-family").value.values[0] ); - return ComputedStyles.of( - [ - ...border, - ["color", Serialise.getLonghand(style, "color")] as const, - ["font-weight", Serialise.getLonghand(style, "font-weight")] as const, - [ - "vertical-align", - Serialise.getLonghand(style, "vertical-align"), - ] as const, - ["background", Serialise.background(style)] as const, - ["outline", Serialise.outline(style)] as const, - ["text-decoration", Serialise.textDecoration(style)] as const, - ["box-shadow", Serialise.boxShadow(style)] as const, - ].filter(([_, value]) => value !== "") + return or( + hasComputedStyle( + "font-weight", + not((weight) => weight.equals(referenceWeight)), + device, + context + ), + hasComputedStyle( + "font-family", + not((family) => Option.from(family.values[0]).equals(referenceFamily)), + device, + context + ) ); } -} -/** - * @internal - */ -export class DistinguishingStyles extends Diagnostic { - public static of( - message: string, - defaultStyles: Iterable> = Sequence.empty(), - hoverStyles: Iterable> = Sequence.empty(), - focusStyles: Iterable> = Sequence.empty() - ): DistinguishingStyles { - return new DistinguishingStyles( - message, - Sequence.from(defaultStyles), - Sequence.from(hoverStyles), - Sequence.from(focusStyles) + function hasDistinguishableVerticalAlign( + container: Element, + device: Device, + context?: Context + ): Predicate { + const reference = Style.from(container, device, context).computed( + "vertical-align" + ).value; + + return hasComputedStyle( + "vertical-align", + not((alignment) => alignment.equals(reference)), + device, + context ); } - private readonly _defaultStyles: Sequence>; - private readonly _hoverStyles: Sequence>; - private readonly _focusStyles: Sequence>; - - private constructor( - message: string, - defaultStyles: Sequence>, - hoverStyles: Sequence>, - focusStyles: Sequence> - ) { - super(message); - this._defaultStyles = defaultStyles; - this._hoverStyles = hoverStyles; - this._focusStyles = focusStyles; - } - - public get defaultStyles(): Iterable> { - return this._defaultStyles; - } - - public get hoverStyles(): Iterable> { - return this._hoverStyles; - } - - public get focusStyles(): Iterable> { - return this._focusStyles; - } - - public equals(value: DistinguishingStyles): boolean; + function hasDistinguishableCursor( + container: Element, + device: Device, + context?: Context + ): Predicate { + // Checking if there is a custom cursor, otherwise grabbing the built-in + function getFirstCursor(style: Style.Computed<"cursor">) { + return style.values[0].values.length !== 0 + ? style.values[0].values[0] + : style.values[1]; + } + const containerCursorStyle = Style.from( + container, + device, + context + ).computed("cursor").value; - public equals(value: unknown): value is this; + // We assume that the first custom cursor, if any, will never fail to load + // and thus don't try to default further. + const reference = getFirstCursor(containerCursorStyle); - public equals(value: unknown): boolean { - return ( - value instanceof DistinguishingStyles && - value._defaultStyles.equals(this._defaultStyles) && - value._hoverStyles.equals(this._hoverStyles) && - value._focusStyles.equals(this._focusStyles) + return hasComputedStyle( + "cursor", + not((cursor) => getFirstCursor(cursor).equals(reference)), + device, + context ); } - - public toJSON(): DistinguishingStyles.JSON { - return { - ...super.toJSON(), - defaultStyle: this._defaultStyles.toJSON(), - hoverStyle: this._hoverStyles.toJSON(), - focusStyle: this._focusStyles.toJSON(), - }; - } -} - -/** - * @internal - */ -export namespace DistinguishingStyles { - export interface JSON extends Diagnostic.JSON { - defaultStyle: Sequence.JSON>; - hoverStyle: Sequence.JSON>; - focusStyle: Sequence.JSON>; - } - - export function isDistinguishingStyles( - value: Diagnostic - ): value is DistinguishingStyles; - - export function isDistinguishingStyles( - value: unknown - ): value is DistinguishingStyles; - - export function isDistinguishingStyles( - value: unknown - ): value is DistinguishingStyles { - return value instanceof DistinguishingStyles; - } } diff --git a/packages/alfa-rules/src/sia-r62/serialise.ts b/packages/alfa-rules/src/sia-r62/serialise.ts index 66a1744511..4751fd6f60 100644 --- a/packages/alfa-rules/src/sia-r62/serialise.ts +++ b/packages/alfa-rules/src/sia-r62/serialise.ts @@ -98,6 +98,34 @@ export namespace Serialise { return serializedShadows.join(", "); } + export function font(style: Style): string { + const optional = (["style", "weight"] as const) + .map((property) => getLonghand(style, `font-${property}`)) + .join(" "); + + const size = style.computed("font-size"); + const family = style.computed("font-family"); + + if (optional !== " ") { + // Optional properties were changed, need to output the mandatory ones. + return normalize(`${optional} ${size} ${family}`); + } + + if ( + size.value.equals(Property.get("font-size").initial.value) && + family.value.values[0].equals( + Property.get("font-family").initial.values[0] + ) + ) { + // Both mandatory properties are set to their initial values. + // Since optional properties also are, we can skip `font` altogether. + return ""; + } + + // Optional properties were not changed but some mandatory ones were. + return normalize(`${size} ${family}`); + } + // Only background-color and background-image are used for deciding if the // link is distinguishable, but all longhands are needed for rendering it // with the correct style. diff --git a/packages/alfa-rules/src/tags/stability.ts b/packages/alfa-rules/src/tags/stability.ts index 3eb96c631e..ab1b58c705 100755 --- a/packages/alfa-rules/src/tags/stability.ts +++ b/packages/alfa-rules/src/tags/stability.ts @@ -60,4 +60,10 @@ export namespace Stability { * reasonably stable; their changes are tracked by normal semver numbering. */ export const Stable = Stability.of("stable"); + + /** + * For deprecated rules. These rules are deprecated and will be removed + * permanently after some grace period. + */ + export const Deprecated = Stability.of("deprecated"); } diff --git a/packages/alfa-rules/test/sia-r62/rule.spec.tsx b/packages/alfa-rules/test/sia-dr62/rule.spec.tsx similarity index 89% rename from packages/alfa-rules/test/sia-r62/rule.spec.tsx rename to packages/alfa-rules/test/sia-dr62/rule.spec.tsx index 2251edf51a..524152ac82 100644 --- a/packages/alfa-rules/test/sia-r62/rule.spec.tsx +++ b/packages/alfa-rules/test/sia-dr62/rule.spec.tsx @@ -3,7 +3,7 @@ import { Err, Ok } from "@siteimprove/alfa-result"; import { Property } from "@siteimprove/alfa-style"; import { test } from "@siteimprove/alfa-test"; -import R62, { ComputedStyles, Outcomes } from "../../src/sia-r62/rule"; +import DR62, { ComputedStyles, Outcomes } from "../../src/sia-dr62/rule"; import { evaluate } from "../common/evaluate"; import { passed, failed, inapplicable } from "../common/outcome"; @@ -51,8 +51,8 @@ test(`evaluate() passes an element with a

parent element with non-link const document = h.document([

Hello {target}

]); - t.deepEqual(await evaluate(R62, { document }), [ - passed(R62, target, { + t.deepEqual(await evaluate(DR62, { document }), [ + passed(DR62, target, { 1: Outcomes.IsDistinguishable( [defaultStyle], [defaultStyle], @@ -72,8 +72,8 @@ test(`evaluate() passes an element with a

parent element with non-link

, ]); - t.deepEqual(await evaluate(R62, { document }), [ - passed(R62, target, { + t.deepEqual(await evaluate(DR62, { document }), [ + passed(DR62, target, { 1: Outcomes.IsDistinguishable( [defaultStyle], [defaultStyle], @@ -99,8 +99,8 @@ test(`evaluate() fails an
element that removes the default text decoration ] ); - t.deepEqual(await evaluate(R62, { document }), [ - failed(R62, target, { + t.deepEqual(await evaluate(DR62, { document }), [ + failed(DR62, target, { 1: Outcomes.IsNotDistinguishable([noStyle], [noStyle], [noStyle]), }), ]); @@ -121,8 +121,8 @@ test(`evaluate() fails an element that removes the default text decoration ] ); - t.deepEqual(await evaluate(R62, { document }), [ - failed(R62, target, { + t.deepEqual(await evaluate(DR62, { document }), [ + failed(DR62, target, { 1: Outcomes.IsNotDistinguishable([defaultStyle], [noStyle], [focusStyle]), }), ]); @@ -145,8 +145,8 @@ test(`evaluate() fails an element that removes the default text decoration ] ); - t.deepEqual(await evaluate(R62, { document }), [ - failed(R62, target, { + t.deepEqual(await evaluate(DR62, { document }), [ + failed(DR62, target, { 1: Outcomes.IsNotDistinguishable( [defaultStyle], [defaultStyle], @@ -173,8 +173,8 @@ test(`evaluate() fails an element that removes the default text decoration ] ); - t.deepEqual(await evaluate(R62, { document }), [ - failed(R62, target, { + t.deepEqual(await evaluate(DR62, { document }), [ + failed(DR62, target, { 1: Outcomes.IsNotDistinguishable([defaultStyle], [noStyle], [noStyle]), }), ]); @@ -200,8 +200,8 @@ test(`evaluate() fails an element that applies a text decoration only on ] ); - t.deepEqual(await evaluate(R62, { document }), [ - failed(R62, target, { + t.deepEqual(await evaluate(DR62, { document }), [ + failed(DR62, target, { 1: Outcomes.IsNotDistinguishable([noStyle], [defaultStyle], [noStyle]), }), ]); @@ -227,8 +227,8 @@ test(`evaluate() fails an element that applies a text decoration only on ] ); - t.deepEqual(await evaluate(R62, { document }), [ - failed(R62, target, { + t.deepEqual(await evaluate(DR62, { document }), [ + failed(DR62, target, { 1: Outcomes.IsNotDistinguishable([noStyle], [noStyle], [defaultStyle]), }), ]); @@ -254,8 +254,8 @@ test(`evaluate() fails an element that applies a text decoration only on ] ); - t.deepEqual(await evaluate(R62, { document }), [ - failed(R62, target, { + t.deepEqual(await evaluate(DR62, { document }), [ + failed(DR62, target, { 1: Outcomes.IsNotDistinguishable( [noStyle], [defaultStyle], @@ -289,8 +289,8 @@ test(`evaluate() passes an applicable element that removes the default text ]) ); - t.deepEqual(await evaluate(R62, { document }), [ - passed(R62, target, { + t.deepEqual(await evaluate(DR62, { document }), [ + passed(DR62, target, { 1: Outcomes.IsDistinguishable([style], [style], [style]), }), ]); @@ -322,8 +322,8 @@ test(`evaluate() passes an applicable element that removes the default text ]) ); - t.deepEqual(await evaluate(R62, { document }), [ - passed(R62, target, { + t.deepEqual(await evaluate(DR62, { document }), [ + passed(DR62, target, { 1: Outcomes.IsDistinguishable( [style], [style], @@ -369,8 +369,8 @@ test(`evaluate() fails an element that has no distinguishing features and ]) ); - t.deepEqual(await evaluate(R62, { document }), [ - failed(R62, target, { + t.deepEqual(await evaluate(DR62, { document }), [ + failed(DR62, target, { 1: Outcomes.IsNotDistinguishable( [style], [style], @@ -416,8 +416,8 @@ test(`evaluate() fails an element that has no distinguishing features and ]) ); - t.deepEqual(await evaluate(R62, { document }), [ - failed(R62, target, { + t.deepEqual(await evaluate(DR62, { document }), [ + failed(DR62, target, { 1: Outcomes.IsNotDistinguishable( [style], [style], @@ -462,8 +462,8 @@ test(`evaluate() passes an applicable element that removes the default text ]) ); - t.deepEqual(await evaluate(R62, { document }), [ - passed(R62, target, { + t.deepEqual(await evaluate(DR62, { document }), [ + passed(DR62, target, { 1: Outcomes.IsDistinguishable( [style], [style], @@ -501,8 +501,8 @@ test(`evaluate() fails an element that has no distinguishing features but is ] ); - t.deepEqual(await evaluate(R62, { document }), [ - failed(R62, target, { + t.deepEqual(await evaluate(DR62, { document }), [ + failed(DR62, target, { 1: Outcomes.IsNotDistinguishable( [noStyle], [noStyle], @@ -549,8 +549,8 @@ test(`evaluate() fails an element that has no distinguishing features and ]) ); - t.deepEqual(await evaluate(R62, { document }), [ - failed(R62, target, { + t.deepEqual(await evaluate(DR62, { document }), [ + failed(DR62, target, { 1: Outcomes.IsNotDistinguishable( [style], [style], @@ -578,7 +578,7 @@ test(`evaluate() is inapplicable to an element with no visible text content` const document = h.document([

Hello {target}

]); - t.deepEqual(await evaluate(R62, { document }), [inapplicable(R62)]); + t.deepEqual(await evaluate(DR62, { document }), [inapplicable(DR62)]); }); test(`evaluate() is inapplicable to an
element with a

parent element @@ -587,7 +587,7 @@ test(`evaluate() is inapplicable to an element with a

parent element const document = h.document([

{target}

]); - t.deepEqual(await evaluate(R62, { document }), [inapplicable(R62)]); + t.deepEqual(await evaluate(DR62, { document }), [inapplicable(DR62)]); }); test(`evaluate() is inapplicable to an element with a

parent element @@ -600,7 +600,7 @@ test(`evaluate() is inapplicable to an element with a

parent element

, ]); - t.deepEqual(await evaluate(R62, { document }), [inapplicable(R62)]); + t.deepEqual(await evaluate(DR62, { document }), [inapplicable(DR62)]); }); test(`evaluate() passes an element with a
parent element @@ -613,8 +613,8 @@ test(`evaluate() passes an element with a
parent elem
, ]); - t.deepEqual(await evaluate(R62, { document }), [ - passed(R62, target, { + t.deepEqual(await evaluate(DR62, { document }), [ + passed(DR62, target, { 1: Outcomes.IsDistinguishable( [defaultStyle], [defaultStyle], @@ -634,7 +634,7 @@ test(`evaluate() is inapplicable to an
element with a

parent element

, ]); - t.deepEqual(await evaluate(R62, { document }), [inapplicable(R62)]); + t.deepEqual(await evaluate(DR62, { document }), [inapplicable(DR62)]); }); test(`evaluate() passes a link whose bolder than surrounding text`, async (t) => { @@ -665,8 +665,8 @@ test(`evaluate() passes a link whose bolder than surrounding text`, async (t) => ]) ); - t.deepEqual(await evaluate(R62, { document }), [ - passed(R62, target, { + t.deepEqual(await evaluate(DR62, { document }), [ + passed(DR62, target, { 1: Outcomes.IsDistinguishable( [style], [style], @@ -696,8 +696,8 @@ test(`evaluates() doesn't break when link text is nested`, async (t) => { const document = h.document([

Hello {target}

]); - t.deepEqual(await evaluate(R62, { document }), [ - passed(R62, target, { + t.deepEqual(await evaluate(DR62, { document }), [ + passed(DR62, target, { 1: Outcomes.IsDistinguishable( [defaultStyle, noStyle], [defaultStyle, noStyle], @@ -741,8 +741,8 @@ test(`evaluates() accepts decoration on children of links`, async (t) => { ]) ); - t.deepEqual(await evaluate(R62, { document }), [ - passed(R62, target, { + t.deepEqual(await evaluate(DR62, { document }), [ + passed(DR62, target, { 1: Outcomes.IsDistinguishable( [style, noStyle], [style, noStyle], @@ -793,8 +793,8 @@ test(`evaluates() accepts decoration on parents of links`, async (t) => { ]) ); - t.deepEqual(await evaluate(R62, { document }), [ - passed(R62, target, { + t.deepEqual(await evaluate(DR62, { document }), [ + passed(DR62, target, { 1: Outcomes.IsDistinguishable( [linkStyle, spanStyle], [linkStyle, spanStyle], @@ -815,8 +815,8 @@ test(`evaluates() deduplicate styles in diagnostic`, async (t) => { const document = h.document([

Hello {target}

]); - t.deepEqual(await evaluate(R62, { document }), [ - passed(R62, target, { + t.deepEqual(await evaluate(DR62, { document }), [ + passed(DR62, target, { 1: Outcomes.IsDistinguishable( [defaultStyle, noStyle], [defaultStyle, noStyle], @@ -853,8 +853,8 @@ test(`evaluates() passes on link with a different background-image than text`, a ]) ); - t.deepEqual(await evaluate(R62, { document }), [ - passed(R62, target, { + t.deepEqual(await evaluate(DR62, { document }), [ + passed(DR62, target, { 1: Outcomes.IsDistinguishable([style], [style], [style]), }), ]); @@ -886,8 +886,8 @@ test(`evaluate() passes an
element in superscript`, async (t) => { ]) ); - t.deepEqual(await evaluate(R62, { document }), [ - passed(R62, target, { + t.deepEqual(await evaluate(DR62, { document }), [ + passed(DR62, target, { 1: Outcomes.IsDistinguishable( [style, noStyle], [style, noStyle], @@ -923,8 +923,8 @@ test(`evaluate() passes an applicable element that removes the default text ]) ); - t.deepEqual(await evaluate(R62, { document }), [ - passed(R62, target, { + t.deepEqual(await evaluate(DR62, { document }), [ + passed(DR62, target, { 1: Outcomes.IsDistinguishable([style], [style], [style]), }), ]); @@ -947,8 +947,8 @@ test(`evaluate() fails an applicable element that removes the default text ] ); - t.deepEqual(await evaluate(R62, { document }), [ - failed(R62, target, { + t.deepEqual(await evaluate(DR62, { document }), [ + failed(DR62, target, { 1: Outcomes.IsNotDistinguishable([noStyle], [noStyle], [noStyle]), }), ]); @@ -960,5 +960,5 @@ test(`evaluate() is inapplicable to an element with a

parent element const document = h.document([

{target}

]); - t.deepEqual(await evaluate(R62, { document }), [inapplicable(R62)]); + t.deepEqual(await evaluate(DR62, { document }), [inapplicable(DR62)]); }); diff --git a/packages/alfa-rules/test/sia-er62/serialise.spec.ts b/packages/alfa-rules/test/sia-dr62/serialise.spec.ts similarity index 70% rename from packages/alfa-rules/test/sia-er62/serialise.spec.ts rename to packages/alfa-rules/test/sia-dr62/serialise.spec.ts index 5478d6906b..f978ed23fd 100644 --- a/packages/alfa-rules/test/sia-er62/serialise.spec.ts +++ b/packages/alfa-rules/test/sia-dr62/serialise.spec.ts @@ -2,9 +2,9 @@ import { Device } from "@siteimprove/alfa-device"; import { Declaration } from "@siteimprove/alfa-dom"; import { Style } from "@siteimprove/alfa-style"; import { test } from "@siteimprove/alfa-test"; -import { Serialise } from "../../src/sia-er62/serialise"; +import { Serialise } from "../../src/sia-dr62/serialise"; -const { background } = Serialise; +const { background, boxShadow } = Serialise; const device = Device.standard(); function mkStyle(properties: Array<[string, string]>): Style { @@ -157,3 +157,59 @@ test(`background correctly shortens background-repeat`, (t) => { } } }); + +test(`boxShadow() serialises a box-shadow that is not set`, (t) => { + const style = mkStyle([["box-shadow", "none"]]); + + t.deepEqual(boxShadow(style), ""); +}); + +test(`boxShadow() serialises a box-shadow that is set to initial`, (t) => { + const style = mkStyle([["box-shadow", "initial"]]); + + t.deepEqual(boxShadow(style), ""); +}); + +test(`boxShadow() serialises a box-shadow with all properties set`, (t) => { + const style = mkStyle([["box-shadow", "1px 2px 3px 4px red inset"]]); + + t.deepEqual(boxShadow(style), "1px 2px 3px 4px rgb(100% 0% 0%) inset"); +}); + +test(`boxShadow() serialises a box-shadow with no color set`, (t) => { + const style = mkStyle([["box-shadow", "1px 2px 0px 0px"]]); + + t.deepEqual(boxShadow(style), "1px 2px"); +}); + +test(`boxShadow() serialises a box-shadow with zero blur and zero spread`, (t) => { + const style = mkStyle([["box-shadow", "1px 2px 0px 0px red"]]); + + t.deepEqual(boxShadow(style), "1px 2px rgb(100% 0% 0%)"); +}); + +test(`boxShadow() serialises a box-shadow with zero blur and non-zero spread`, (t) => { + const style = mkStyle([["box-shadow", "1px 2px 0px 1px red"]]); + + t.deepEqual(boxShadow(style), "1px 2px 0px 1px rgb(100% 0% 0%)"); +}); + +test(`boxShadow() serialises a box-shadow with non-zero blur and zero spread`, (t) => { + const style = mkStyle([["box-shadow", "1px 2px 1px 0px red"]]); + + t.deepEqual(boxShadow(style), "1px 2px 1px rgb(100% 0% 0%)"); +}); + +test(`boxShadow() serialises a box-shadow with multiple values`, (t) => { + const style = mkStyle([ + [ + "box-shadow", + "1px 2px 0px 1px red, 1px 2px 0px 1px blue, 1px 2px 0px 1px black", + ], + ]); + + t.deepEqual( + boxShadow(style), + "1px 2px 0px 1px rgb(100% 0% 0%), 1px 2px 0px 1px rgb(0% 0% 100%), 1px 2px 0px 1px rgb(0% 0% 0%)" + ); +}); diff --git a/packages/alfa-rules/test/sia-er62/common.ts b/packages/alfa-rules/test/sia-r62/common.ts similarity index 97% rename from packages/alfa-rules/test/sia-er62/common.ts rename to packages/alfa-rules/test/sia-r62/common.ts index 2ac7559751..6453226b35 100644 --- a/packages/alfa-rules/test/sia-er62/common.ts +++ b/packages/alfa-rules/test/sia-r62/common.ts @@ -1,7 +1,7 @@ import { Percentage, RGB } from "@siteimprove/alfa-css"; import { Err, Ok, Result } from "@siteimprove/alfa-result"; import { Contrast } from "../../src/common/diagnostic/contrast"; -import { ElementDistinguishable } from "../../src/sia-er62/diagnostics"; +import { ElementDistinguishable } from "../../src/sia-r62/diagnostics"; export function addCursor( style: Result diff --git a/packages/alfa-rules/test/sia-er62/rule.applicability.spec.tsx b/packages/alfa-rules/test/sia-r62/rule.applicability.spec.tsx similarity index 88% rename from packages/alfa-rules/test/sia-er62/rule.applicability.spec.tsx rename to packages/alfa-rules/test/sia-r62/rule.applicability.spec.tsx index 4c4019007a..908c52e61e 100644 --- a/packages/alfa-rules/test/sia-er62/rule.applicability.spec.tsx +++ b/packages/alfa-rules/test/sia-r62/rule.applicability.spec.tsx @@ -2,7 +2,7 @@ import { Percentage, RGB } from "@siteimprove/alfa-css"; import { h } from "@siteimprove/alfa-dom"; import { Err, Ok } from "@siteimprove/alfa-result"; import { test } from "@siteimprove/alfa-test"; -import ER62, { Outcomes } from "../../src/sia-er62/rule"; +import R62, { Outcomes } from "../../src/sia-r62/rule"; import { evaluate } from "../common/evaluate"; import { inapplicable, passed } from "../common/outcome"; import { Defaults, addCursor, addOutline, makePairing } from "./common"; @@ -24,8 +24,8 @@ test(`evaluate() is applicable to an
element with a

parent element with const document = h.document([

Hello {target}

]); - t.deepEqual(await evaluate(ER62, { document }), [ - passed(ER62, target, { + t.deepEqual(await evaluate(R62, { document }), [ + passed(R62, target, { 1: Outcomes.IsDistinguishable([defaultStyle], [hoverStyle], [focusStyle]), }), ]); @@ -41,8 +41,8 @@ test(`evaluate() is applicable to an
element with a

parent element with

, ]); - t.deepEqual(await evaluate(ER62, { document }), [ - passed(ER62, target, { + t.deepEqual(await evaluate(R62, { document }), [ + passed(R62, target, { 1: Outcomes.IsDistinguishable([defaultStyle], [hoverStyle], [focusStyle]), }), ]); @@ -58,8 +58,8 @@ test(`evaluate() is applicable to an
element with a
p
, ]); - t.deepEqual(await evaluate(ER62, { document }), [ - passed(ER62, target, { + t.deepEqual(await evaluate(R62, { document }), [ + passed(R62, target, { 1: Outcomes.IsDistinguishable([defaultStyle], [hoverStyle], [focusStyle]), }), ]); @@ -96,8 +96,8 @@ test(`evaluate() is applicable to an
element with a

parent element ]) ); - t.deepEqual(await evaluate(ER62, { document }), [ - passed(ER62, target, { + t.deepEqual(await evaluate(R62, { document }), [ + passed(R62, target, { 1: Outcomes.IsDistinguishable( [style], [addCursor(style)], @@ -136,8 +136,8 @@ test(`evaluate() is applicable to an element with a

parent element ] ); - t.deepEqual(await evaluate(ER62, { document }), [ - passed(ER62, target, { + t.deepEqual(await evaluate(R62, { document }), [ + passed(R62, target, { 1: Outcomes.IsDistinguishable([defaultStyle], [hoverStyle], [focusStyle]), }), ]); @@ -174,8 +174,8 @@ test(`evaluate() is applicable to an element with a

parent element .withStyle(["color", "rgb(0% 0% 0%)"]) ); - t.deepEqual(await evaluate(ER62, { document }), [ - passed(ER62, target, { + t.deepEqual(await evaluate(R62, { document }), [ + passed(R62, target, { 1: Outcomes.IsDistinguishable( [defaultStyle, spanStyle], [hoverStyle, spanStyle], @@ -224,8 +224,8 @@ test(`evaluate() is applicable to an element with a

parent element .withPairings([makePairing(defaultLinkColor, defaultTextColor, 2.23)]) .withStyle(["color", "rgb(0% 0% 0%)"]); - t.deepEqual(await evaluate(ER62, { document }), [ - passed(ER62, target, { + t.deepEqual(await evaluate(R62, { document }), [ + passed(R62, target, { 1: Outcomes.IsDistinguishable( [style, Err.of(spanStyle)], [addCursor(Ok.of(spanStyle)), addCursor(style)], @@ -262,8 +262,8 @@ test(`evaluate() is applicable to an element when there is a

parent elem ] ); - t.deepEqual(await evaluate(ER62, { document }), [ - passed(ER62, target2, { + t.deepEqual(await evaluate(R62, { document }), [ + passed(R62, target2, { 1: Outcomes.IsDistinguishable([defaultStyle], [hoverStyle], [focusStyle]), }), ]); @@ -291,11 +291,11 @@ test(`evaluate() is applicable to several elements when there is a

paren ] ); - t.deepEqual(await evaluate(ER62, { document }), [ - passed(ER62, target1, { + t.deepEqual(await evaluate(R62, { document }), [ + passed(R62, target1, { 1: Outcomes.IsDistinguishable([defaultStyle], [hoverStyle], [focusStyle]), }), - passed(ER62, target2, { + passed(R62, target2, { 1: Outcomes.IsDistinguishable([defaultStyle], [hoverStyle], [focusStyle]), }), ]); @@ -368,8 +368,8 @@ test(`evaluate() is applicable to an element with a

parent element that .withDistinguishingProperties(["background", "text-decoration"]) ); - t.deepEqual(await evaluate(ER62, { document }), [ - passed(ER62, target, { + t.deepEqual(await evaluate(R62, { document }), [ + passed(R62, target, { 1: Outcomes.IsDistinguishable( [style], [addCursor(style)], @@ -388,7 +388,7 @@ test(`evaluate() is inapplicable to an element with no visible text content` const document = h.document([

Hello {target}

]); - t.deepEqual(await evaluate(ER62, { document }), [inapplicable(ER62)]); + t.deepEqual(await evaluate(R62, { document }), [inapplicable(R62)]); }); test(`evaluate() is inapplicable to an element with a

parent element @@ -397,7 +397,7 @@ test(`evaluate() is inapplicable to an element with a

parent element const document = h.document([

{target}

]); - t.deepEqual(await evaluate(ER62, { document }), [inapplicable(ER62)]); + t.deepEqual(await evaluate(R62, { document }), [inapplicable(R62)]); }); test(`evaluate() is inapplicable to an element with a

parent element @@ -410,7 +410,7 @@ test(`evaluate() is inapplicable to an element with a

parent element

, ]); - t.deepEqual(await evaluate(ER62, { document }), [inapplicable(ER62)]); + t.deepEqual(await evaluate(R62, { document }), [inapplicable(R62)]); }); test(`evaluate() is inapplicable to an element with a

parent element @@ -423,7 +423,7 @@ test(`evaluate() is inapplicable to an element with a

parent element

, ]); - t.deepEqual(await evaluate(ER62, { document }), [inapplicable(ER62)]); + t.deepEqual(await evaluate(R62, { document }), [inapplicable(R62)]); }); test(`evaluate() is inapplicable to an element with a

parent element @@ -432,7 +432,7 @@ test(`evaluate() is inapplicable to an element with a

parent element const document = h.document([

{target}

]); - t.deepEqual(await evaluate(ER62, { document }), [inapplicable(ER62)]); + t.deepEqual(await evaluate(R62, { document }), [inapplicable(R62)]); }); test(`evaluate() is inapplicable to an element with a

parent element @@ -450,7 +450,7 @@ test(`evaluate() is inapplicable to an element with a

parent element ] ); - t.deepEqual(await evaluate(ER62, { document }), [inapplicable(ER62)]); + t.deepEqual(await evaluate(R62, { document }), [inapplicable(R62)]); }); test(`evaluate() is inapplicable to an element with a

parent element @@ -473,7 +473,7 @@ test(`evaluate() is inapplicable to an element with a

parent element ] ); - t.deepEqual(await evaluate(ER62, { document }), [inapplicable(ER62)]); + t.deepEqual(await evaluate(R62, { document }), [inapplicable(R62)]); }); test(`evaluates() is inapplicable when the only non-link text is in a nested paragraph`, async (t) => { @@ -488,7 +488,7 @@ test(`evaluates() is inapplicable when the only non-link text is in a nested par

, ]); - t.deepEqual(await evaluate(ER62, { document }), [inapplicable(ER62)]); + t.deepEqual(await evaluate(R62, { document }), [inapplicable(R62)]); }); test(`evaluates() is inapplicable when there is no non-link text in a nested paragraph`, async (t) => { @@ -506,5 +506,5 @@ test(`evaluates() is inapplicable when there is no non-link text in a nested par , ]); - t.deepEqual(await evaluate(ER62, { document }), [inapplicable(ER62)]); + t.deepEqual(await evaluate(R62, { document }), [inapplicable(R62)]); }); diff --git a/packages/alfa-rules/test/sia-er62/rule.expectation.contrast.spec.tsx b/packages/alfa-rules/test/sia-r62/rule.expectation.contrast.spec.tsx similarity index 94% rename from packages/alfa-rules/test/sia-er62/rule.expectation.contrast.spec.tsx rename to packages/alfa-rules/test/sia-r62/rule.expectation.contrast.spec.tsx index 5480e5be64..021a990308 100644 --- a/packages/alfa-rules/test/sia-er62/rule.expectation.contrast.spec.tsx +++ b/packages/alfa-rules/test/sia-r62/rule.expectation.contrast.spec.tsx @@ -2,7 +2,7 @@ import { Percentage, RGB } from "@siteimprove/alfa-css"; import { h } from "@siteimprove/alfa-dom"; import { Err, Ok } from "@siteimprove/alfa-result"; import { test } from "@siteimprove/alfa-test"; -import ER62, { Outcomes } from "../../src/sia-er62/rule"; +import R62, { Outcomes } from "../../src/sia-r62/rule"; import { evaluate } from "../common/evaluate"; import { failed, passed } from "../common/outcome"; import { Defaults, makePairing } from "./common"; @@ -79,8 +79,8 @@ test(`evaluate() passes an element that has a difference in contrast of 3:1 .withDistinguishingProperties(["contrast"]) ); - t.deepEqual(await evaluate(ER62, { document }), [ - passed(ER62, target, { + t.deepEqual(await evaluate(R62, { document }), [ + passed(R62, target, { 1: Outcomes.IsDistinguishable([style], [style], [style]), }), ]); @@ -151,8 +151,8 @@ test(`evaluate() passes an element that is distinguishable from the

pare .withDistinguishingProperties(["contrast"]) ); - t.deepEqual(await evaluate(ER62, { document }), [ - passed(ER62, target, { + t.deepEqual(await evaluate(R62, { document }), [ + passed(R62, target, { 1: Outcomes.IsDistinguishable([style], [style], [style]), }), ]); @@ -198,8 +198,8 @@ test(`evaluate() passes an element that is distinguishable from the

pare .withDistinguishingProperties(["contrast"]) ); - t.deepEqual(await evaluate(ER62, { document }), [ - passed(ER62, target, { + t.deepEqual(await evaluate(R62, { document }), [ + passed(R62, target, { 1: Outcomes.IsDistinguishable([style], [style], [style]), }), ]); @@ -241,8 +241,8 @@ test(`evaluate() fails an element that is not distinguishable from the

p .withPairings(contrastPairings) ); - t.deepEqual(await evaluate(ER62, { document }), [ - failed(ER62, target, { + t.deepEqual(await evaluate(R62, { document }), [ + failed(R62, target, { 1: Outcomes.IsNotDistinguishable([noStyle], [noStyle], [noStyle]), }), ]); @@ -287,8 +287,8 @@ test(`evaluate() fails an element that is not distinguishable from the

p .withPairings(contrastPairings) ); - t.deepEqual(await evaluate(ER62, { document }), [ - failed(ER62, target, { + t.deepEqual(await evaluate(R62, { document }), [ + failed(R62, target, { 1: Outcomes.IsNotDistinguishable([noStyle], [noStyle], [noStyle]), }), ]); diff --git a/packages/alfa-rules/test/sia-er62/rule.expectation.spec.tsx b/packages/alfa-rules/test/sia-r62/rule.expectation.spec.tsx similarity index 90% rename from packages/alfa-rules/test/sia-er62/rule.expectation.spec.tsx rename to packages/alfa-rules/test/sia-r62/rule.expectation.spec.tsx index f81716c52b..bc9cd67104 100644 --- a/packages/alfa-rules/test/sia-er62/rule.expectation.spec.tsx +++ b/packages/alfa-rules/test/sia-r62/rule.expectation.spec.tsx @@ -1,8 +1,8 @@ import { h } from "@siteimprove/alfa-dom"; import { Err, Ok } from "@siteimprove/alfa-result"; import { test } from "@siteimprove/alfa-test"; -import { ElementDistinguishable } from "../../src/sia-er62/diagnostics"; -import ER62, { Outcomes } from "../../src/sia-er62/rule"; +import { ElementDistinguishable } from "../../src/sia-r62/diagnostics"; +import R62, { Outcomes } from "../../src/sia-r62/rule"; import { evaluate } from "../common/evaluate"; import { failed, passed } from "../common/outcome"; import { Defaults, addCursor, addOutline } from "./common"; @@ -44,8 +44,8 @@ test(`evaluates() passes on link with a different background-image than text`, a .withDistinguishingProperties(["background"]) ); - t.deepEqual(await evaluate(ER62, { document }), [ - passed(ER62, target, { + t.deepEqual(await evaluate(R62, { document }), [ + passed(R62, target, { 1: Outcomes.IsDistinguishable( [style], [addCursor(style)], @@ -77,8 +77,8 @@ test(`evaluate() passes an applicable element that removes the default text .withDistinguishingProperties(["background"]) ); - t.deepEqual(await evaluate(ER62, { document }), [ - passed(ER62, target, { + t.deepEqual(await evaluate(R62, { document }), [ + passed(R62, target, { 1: Outcomes.IsDistinguishable( [style], [addCursor(style)], @@ -107,8 +107,8 @@ test(`evaluate() fails an element that has no distinguishing features and is ] ); - t.deepEqual(await evaluate(ER62, { document }), [ - failed(ER62, target, { + t.deepEqual(await evaluate(R62, { document }), [ + failed(R62, target, { 1: Outcomes.IsNotDistinguishable( [noStyle], [addCursor(noStyle)], @@ -138,8 +138,8 @@ test(`evaluate() fails an element that has no distinguishing features and is ] ); - t.deepEqual(await evaluate(ER62, { document }), [ - failed(ER62, target, { + t.deepEqual(await evaluate(R62, { document }), [ + failed(R62, target, { 1: Outcomes.IsNotDistinguishable( [noStyle], [addCursor(noStyle)], @@ -173,8 +173,8 @@ test(`evaluate() fails an element that has no distinguishing features and noDistinguishingProperties.withStyle(["background", "rgb(100% 0% 0%)"]) ); - t.deepEqual(await evaluate(ER62, { document }), [ - failed(ER62, target, { + t.deepEqual(await evaluate(R62, { document }), [ + failed(R62, target, { 1: Outcomes.IsNotDistinguishable( [style], [addCursor(style)], @@ -213,8 +213,8 @@ test(`evaluate() passes an applicable element that removes the default text .withDistinguishingProperties(["box-shadow"]) ); - t.deepEqual(await evaluate(ER62, { document }), [ - passed(ER62, target, { + t.deepEqual(await evaluate(R62, { document }), [ + passed(R62, target, { 1: Outcomes.IsDistinguishable([style], [addCursor(style)], [style]), }), ]); @@ -237,8 +237,8 @@ test(`evaluate() fails an applicable element that removes the default text ] ); - t.deepEqual(await evaluate(ER62, { document }), [ - failed(ER62, target, { + t.deepEqual(await evaluate(R62, { document }), [ + failed(R62, target, { 1: Outcomes.IsNotDistinguishable( [noStyle], [addCursor(noStyle)], @@ -281,8 +281,8 @@ test(`evaluate() passes an applicable element that removes the default text .withDistinguishingProperties(["border"]) ); - t.deepEqual(await evaluate(ER62, { document }), [ - passed(ER62, target, { + t.deepEqual(await evaluate(R62, { document }), [ + passed(R62, target, { 1: Outcomes.IsDistinguishable( [style], [addCursor(style)], @@ -316,8 +316,8 @@ test(`evaluate() fails an element that has no distinguishing features and ) ); - t.deepEqual(await evaluate(ER62, { document }), [ - failed(ER62, target, { + t.deepEqual(await evaluate(R62, { document }), [ + failed(R62, target, { 1: Outcomes.IsNotDistinguishable( [style], [addCursor(style)], @@ -351,8 +351,8 @@ test(`evaluate() fails an element that has no distinguishing features and ) ); - t.deepEqual(await evaluate(ER62, { document }), [ - failed(ER62, target, { + t.deepEqual(await evaluate(R62, { document }), [ + failed(R62, target, { 1: Outcomes.IsNotDistinguishable( [style], [addCursor(style)], @@ -392,8 +392,8 @@ test(`evaluate() passes a link whose bolder than surrounding text`, async (t) => .withDistinguishingProperties(["font"]) ); - t.deepEqual(await evaluate(ER62, { document }), [ - passed(ER62, target, { + t.deepEqual(await evaluate(R62, { document }), [ + passed(R62, target, { 1: Outcomes.IsDistinguishable( [style], [addCursor(style)], @@ -428,8 +428,8 @@ test(`evaluate() passes a link with different font-family than surrounding text` .withDistinguishingProperties(["font"]) ); - t.deepEqual(await evaluate(ER62, { document }), [ - passed(ER62, target, { + t.deepEqual(await evaluate(R62, { document }), [ + passed(R62, target, { 1: Outcomes.IsDistinguishable( [style], [addCursor(style)], @@ -473,8 +473,8 @@ test(`evaluate() passes an applicable element that removes the default text ) ); - t.deepEqual(await evaluate(ER62, { document }), [ - passed(ER62, target, { + t.deepEqual(await evaluate(R62, { document }), [ + passed(R62, target, { 1: Outcomes.IsDistinguishable([style], [addCursor(style)], [style]), }), ]); @@ -497,8 +497,8 @@ test(`evaluate() fails an element that removes the default text decoration ] ); - t.deepEqual(await evaluate(ER62, { document }), [ - failed(ER62, target, { + t.deepEqual(await evaluate(R62, { document }), [ + failed(R62, target, { 1: Outcomes.IsNotDistinguishable([defaultStyle], [hoverStyle], [noStyle]), }), ]); @@ -522,8 +522,8 @@ test(`evaluate() fails an element that removes the default text decoration ] ); - t.deepEqual(await evaluate(ER62, { document }), [ - failed(ER62, target, { + t.deepEqual(await evaluate(R62, { document }), [ + failed(R62, target, { 1: Outcomes.IsNotDistinguishable([defaultStyle], [noStyle], [noStyle]), }), ]); @@ -559,8 +559,8 @@ test(`evaluate() passes an element in superscript`, async (t) => { .withDistinguishingProperties(["vertical-align"]) ); - t.deepEqual(await evaluate(ER62, { document }), [ - passed(ER62, target, { + t.deepEqual(await evaluate(R62, { document }), [ + passed(R62, target, { 1: Outcomes.IsDistinguishable( [style, noStyle], [addCursor(style), addCursor(noStyle)], @@ -607,8 +607,8 @@ test(`evaluate() passes an element with a

parent element when ]) ); - t.deepEqual(await evaluate(ER62, { document }), [ - passed(ER62, target, { + t.deepEqual(await evaluate(R62, { document }), [ + passed(R62, target, { 1: Outcomes.IsDistinguishable( [style], [addCursor(style)], diff --git a/packages/alfa-rules/test/sia-er62/rule.expectation.textdecoration.spec.tsx b/packages/alfa-rules/test/sia-r62/rule.expectation.textdecoration.spec.tsx similarity index 87% rename from packages/alfa-rules/test/sia-er62/rule.expectation.textdecoration.spec.tsx rename to packages/alfa-rules/test/sia-r62/rule.expectation.textdecoration.spec.tsx index 348d3299ac..d62a232866 100644 --- a/packages/alfa-rules/test/sia-er62/rule.expectation.textdecoration.spec.tsx +++ b/packages/alfa-rules/test/sia-r62/rule.expectation.textdecoration.spec.tsx @@ -1,8 +1,8 @@ import { h } from "@siteimprove/alfa-dom"; import { Ok } from "@siteimprove/alfa-result"; import { test } from "@siteimprove/alfa-test"; -import { ElementDistinguishable } from "../../src/sia-er62/diagnostics"; -import ER62, { Outcomes } from "../../src/sia-er62/rule"; +import { ElementDistinguishable } from "../../src/sia-r62/diagnostics"; +import R62, { Outcomes } from "../../src/sia-r62/rule"; import { evaluate } from "../common/evaluate"; import { failed, passed } from "../common/outcome"; import { Defaults, addCursor, makePairing } from "./common"; @@ -49,8 +49,8 @@ test(`evaluates() accepts decoration on children of links`, async (t) => { .withDistinguishingProperties(["font"]) ); - t.deepEqual(await evaluate(ER62, { document }), [ - passed(ER62, target, { + t.deepEqual(await evaluate(R62, { document }), [ + passed(R62, target, { 1: Outcomes.IsDistinguishable( [style, noStyle], [style, noStyle], @@ -71,8 +71,8 @@ test(`evaluates() doesn't break when link text is nested`, async (t) => { const document = h.document([

Hello {target}

]); - t.deepEqual(await evaluate(ER62, { document }), [ - passed(ER62, target, { + t.deepEqual(await evaluate(R62, { document }), [ + passed(R62, target, { 1: Outcomes.IsDistinguishable( [defaultStyle, noStyle], [hoverStyle, addCursor(noStyle)], @@ -123,8 +123,8 @@ test(`evaluates() accepts decoration on parents of links`, async (t) => { ) ); - t.deepEqual(await evaluate(ER62, { document }), [ - passed(ER62, target, { + t.deepEqual(await evaluate(R62, { document }), [ + passed(R62, target, { 1: Outcomes.IsDistinguishable( [linkStyle, spanStyle], [linkStyle, spanStyle], @@ -143,8 +143,8 @@ test(`evaluates() deduplicate styles in diagnostic`, async (t) => { const document = h.document([

Hello {target}

]); - t.deepEqual(await evaluate(ER62, { document }), [ - passed(ER62, target, { + t.deepEqual(await evaluate(R62, { document }), [ + passed(R62, target, { 1: Outcomes.IsDistinguishable( [defaultStyle, noStyle], [hoverStyle, addCursor(noStyle)], @@ -171,8 +171,8 @@ test(`evaluate() fails an
element that removes the default text decoration ] ); - t.deepEqual(await evaluate(ER62, { document }), [ - failed(ER62, target, { + t.deepEqual(await evaluate(R62, { document }), [ + failed(R62, target, { 1: Outcomes.IsNotDistinguishable([noStyle], [noStyle], [noStyle]), }), ]); @@ -194,8 +194,8 @@ test(`evaluate() fails an element that removes the default text decoration ] ); - t.deepEqual(await evaluate(ER62, { document }), [ - failed(ER62, target, { + t.deepEqual(await evaluate(R62, { document }), [ + failed(R62, target, { 1: Outcomes.IsNotDistinguishable([defaultStyle], [noStyle], [focusStyle]), }), ]); @@ -222,8 +222,8 @@ test(`evaluate() fails an element that applies a text decoration only on ] ); - t.deepEqual(await evaluate(ER62, { document }), [ - failed(ER62, target, { + t.deepEqual(await evaluate(R62, { document }), [ + failed(R62, target, { 1: Outcomes.IsNotDistinguishable([noStyle], [defaultStyle], [noStyle]), }), ]); @@ -250,8 +250,8 @@ test(`evaluate() fails an element that applies a text decoration only on ] ); - t.deepEqual(await evaluate(ER62, { document }), [ - failed(ER62, target, { + t.deepEqual(await evaluate(R62, { document }), [ + failed(R62, target, { 1: Outcomes.IsNotDistinguishable([noStyle], [noStyle], [defaultStyle]), }), ]); @@ -278,8 +278,8 @@ test(`evaluate() fails an element that applies a text decoration only on ] ); - t.deepEqual(await evaluate(ER62, { document }), [ - failed(ER62, target, { + t.deepEqual(await evaluate(R62, { document }), [ + failed(R62, target, { 1: Outcomes.IsNotDistinguishable( [noStyle], [defaultStyle], diff --git a/packages/alfa-rules/test/sia-r62/serialise.spec.ts b/packages/alfa-rules/test/sia-r62/serialise.spec.ts index a7be26a6a5..619c7b9620 100644 --- a/packages/alfa-rules/test/sia-r62/serialise.spec.ts +++ b/packages/alfa-rules/test/sia-r62/serialise.spec.ts @@ -4,7 +4,7 @@ import { Style } from "@siteimprove/alfa-style"; import { test } from "@siteimprove/alfa-test"; import { Serialise } from "../../src/sia-r62/serialise"; -const { background, boxShadow } = Serialise; +const { background } = Serialise; const device = Device.standard(); function mkStyle(properties: Array<[string, string]>): Style { @@ -157,59 +157,3 @@ test(`background correctly shortens background-repeat`, (t) => { } } }); - -test(`boxShadow() serialises a box-shadow that is not set`, (t) => { - const style = mkStyle([["box-shadow", "none"]]); - - t.deepEqual(boxShadow(style), ""); -}); - -test(`boxShadow() serialises a box-shadow that is set to initial`, (t) => { - const style = mkStyle([["box-shadow", "initial"]]); - - t.deepEqual(boxShadow(style), ""); -}); - -test(`boxShadow() serialises a box-shadow with all properties set`, (t) => { - const style = mkStyle([["box-shadow", "1px 2px 3px 4px red inset"]]); - - t.deepEqual(boxShadow(style), "1px 2px 3px 4px rgb(100% 0% 0%) inset"); -}); - -test(`boxShadow() serialises a box-shadow with no color set`, (t) => { - const style = mkStyle([["box-shadow", "1px 2px 0px 0px"]]); - - t.deepEqual(boxShadow(style), "1px 2px"); -}); - -test(`boxShadow() serialises a box-shadow with zero blur and zero spread`, (t) => { - const style = mkStyle([["box-shadow", "1px 2px 0px 0px red"]]); - - t.deepEqual(boxShadow(style), "1px 2px rgb(100% 0% 0%)"); -}); - -test(`boxShadow() serialises a box-shadow with zero blur and non-zero spread`, (t) => { - const style = mkStyle([["box-shadow", "1px 2px 0px 1px red"]]); - - t.deepEqual(boxShadow(style), "1px 2px 0px 1px rgb(100% 0% 0%)"); -}); - -test(`boxShadow() serialises a box-shadow with non-zero blur and zero spread`, (t) => { - const style = mkStyle([["box-shadow", "1px 2px 1px 0px red"]]); - - t.deepEqual(boxShadow(style), "1px 2px 1px rgb(100% 0% 0%)"); -}); - -test(`boxShadow() serialises a box-shadow with multiple values`, (t) => { - const style = mkStyle([ - [ - "box-shadow", - "1px 2px 0px 1px red, 1px 2px 0px 1px blue, 1px 2px 0px 1px black", - ], - ]); - - t.deepEqual( - boxShadow(style), - "1px 2px 0px 1px rgb(100% 0% 0%), 1px 2px 0px 1px rgb(0% 0% 100%), 1px 2px 0px 1px rgb(0% 0% 0%)" - ); -}); diff --git a/packages/alfa-rules/tsconfig.json b/packages/alfa-rules/tsconfig.json index 3da054e828..bcc6c2600a 100644 --- a/packages/alfa-rules/tsconfig.json +++ b/packages/alfa-rules/tsconfig.json @@ -33,12 +33,12 @@ "src/common/predicate/is-whitespace.ts", "src/common/predicate/is-wide-enough.ts", "src/common/predicate/reference-same-resource.ts", + "src/deprecated.ts", "src/experimental.ts", "src/index.ts", "src/rules.ts", - "src/sia-er62/rule.ts", - "src/sia-er62/diagnostics.ts", - "src/sia-er62/serialise.ts", + "src/sia-dr62/rule.ts", + "src/sia-dr62/serialise.ts", "src/sia-er87/rule.ts", "src/sia-r1/rule.ts", "src/sia-r10/rule.ts", @@ -95,6 +95,7 @@ "src/sia-r6/rule.ts", "src/sia-r60/rule.ts", "src/sia-r61/rule.ts", + "src/sia-r62/diagnostics.ts", "src/sia-r62/rule.ts", "src/sia-r62/serialise.ts", "src/sia-r63/rule.ts", @@ -143,12 +144,8 @@ "test/common/outcome.ts", "test/common/dom/get-colors.spec.tsx", "test/common/predicate/is-at-the-start.spec.tsx", - "test/sia-er62/rule.applicability.spec.tsx", - "test/sia-er62/rule.expectation.spec.tsx", - "test/sia-er62/rule.expectation.contrast.spec.tsx", - "test/sia-er62/rule.expectation.textdecoration.spec.tsx", - "test/sia-er62/serialise.spec.ts", - "test/sia-er62/common.ts", + "test/sia-dr62/rule.spec.tsx", + "test/sia-dr62/serialise.spec.ts", "test/sia-er87/rule.spec.tsx", "test/sia-r1/rule.spec.tsx", "test/sia-r2/rule.spec.tsx", @@ -208,7 +205,11 @@ "test/sia-r59/rule.spec.tsx", "test/sia-r60/rule.spec.tsx", "test/sia-r61/rule.spec.tsx", - "test/sia-r62/rule.spec.tsx", + "test/sia-r62/common.ts", + "test/sia-r62/rule.applicability.spec.tsx", + "test/sia-r62/rule.expectation.spec.tsx", + "test/sia-r62/rule.expectation.contrast.spec.tsx", + "test/sia-r62/rule.expectation.textdecoration.spec.tsx", "test/sia-r62/serialise.spec.ts", "test/sia-r63/rule.spec.tsx", "test/sia-r64/rule.spec.tsx",