diff --git a/.github/workflows/integrate.yml b/.github/workflows/integrate.yml index 456fd09abb..27a153c2a8 100644 --- a/.github/workflows/integrate.yml +++ b/.github/workflows/integrate.yml @@ -11,9 +11,11 @@ jobs: name: Build strategy: matrix: - os: [ubuntu-latest, windows-latest] + os: [ ubuntu-latest, windows-latest ] node: [12, 14] runs-on: ${{ matrix.os }} + env: + NODE_OPTIONS: "--max-old-space-size=4096" steps: - uses: actions/checkout@v2.3.4 - uses: actions/setup-node@v2.1.5 diff --git a/README.md b/README.md index 58e2ba2d57..6a4d3553a8 100644 --- a/README.md +++ b/README.md @@ -148,6 +148,18 @@ As new code is pulled from the repository, changes to dependencies and code may $ yarn build ``` +If you want to run tests and make sure everything is working, use: + +```console +$ yarn test +``` + +When working on a specific package, you can run only these tests: + +```console +$ yarn test packages/alfa- +``` + If you would like to contribute to Alfa, make sure to check out the [contribution guidelines](docs/contributing.md). If you have any questions, you are also welcome to [open an issue][]. ## Architecture diff --git a/packages/alfa-aria/src/name.ts b/packages/alfa-aria/src/name.ts index 0c5bec8d55..6ce110a9f8 100644 --- a/packages/alfa-aria/src/name.ts +++ b/packages/alfa-aria/src/name.ts @@ -610,7 +610,13 @@ export namespace Name { // https://w3c.github.io/accname/#step1 // Step 1 is skipped when referencing due to step 2B.ii.b // https://w3c.github.io/accname/#step2B.ii.b - if (!state.isReferencing && role.some((role) => role.isNameProhibited())) { + // Step 1 is skipped when descending due to step 2F.iii.b + // https://w3c.github.io/accname/#step2B.iii.b + if ( + !state.isReferencing && + !state.isDescending && + role.some((role) => role.isNameProhibited()) + ) { return None; } @@ -736,7 +742,7 @@ export namespace Name { ) ); - const name = flatten(names.map((name) => name.value).join(" ")); + const name = flatten(names.map((name) => name.value).join(" ")).trim(); if (name === "") { return None; diff --git a/packages/alfa-aria/test/name.spec.tsx b/packages/alfa-aria/test/name.spec.tsx index 4cd88c9bf0..222df4766e 100644 --- a/packages/alfa-aria/test/name.spec.tsx +++ b/packages/alfa-aria/test/name.spec.tsx @@ -361,6 +361,27 @@ test(`.from() determines the name of an element with a child element }); }); +test(`.from() rejects whitespace only content and defaults to next step`, (t) => { + const a = ( + + + + ); + + t.deepEqual(Name.from(a, device).toJSON(), { + type: "some", + value: { + value: "Hello world", + sources: [ + { + type: "label", + attribute: "/a[1]/@title", + }, + ], + }, + }); +}); + test(`.from() determines the name of an element with a
child element with a child element with an alt attribute`, (t) => { const a = ( @@ -473,6 +494,86 @@ test(`.from() determines the name of an element with text in its subtree, }); }); +test(`.from() determines the name of an element with text in its subtree, + when the source is nested and doesn't itself allow naming`, (t) => { + const a = ( + +

Hello world

+
+ ); + + t.deepEqual(Name.from(a, device).toJSON(), { + type: "some", + value: { + value: "Hello world", + sources: [ + { + type: "descendant", + element: "/a[1]", + name: { + value: "Hello world", + sources: [ + { + type: "descendant", + element: "/a[1]/p[1]", + name: { + value: "Hello world", + sources: [ + { + type: "data", + text: "/a[1]/p[1]/text()[1]", + }, + ], + }, + }, + ], + }, + }, + ], + }, + }); +}); + +test(`.from() determines the name of an element with text in its subtree, + when the source is nested and presentational`, (t) => { + const a = ( + + Hello world + + ); + + t.deepEqual(Name.from(a, device).toJSON(), { + type: "some", + value: { + value: "Hello world", + sources: [ + { + type: "descendant", + element: "/a[1]", + name: { + value: "Hello world", + sources: [ + { + type: "descendant", + element: "/a[1]/span[1]", + name: { + value: "Hello world", + sources: [ + { + type: "data", + text: "/a[1]/span[1]/text()[1]", + }, + ], + }, + }, + ], + }, + }, + ], + }, + }); +}); + test(`.from() determines the name of an element with text in its subtree, when there are multiple nested sources`, (t) => { const a = ( diff --git a/packages/alfa-css/src/syntax/nth.ts b/packages/alfa-css/src/syntax/nth.ts index 2ea1dfc18f..091c170f0e 100644 --- a/packages/alfa-css/src/syntax/nth.ts +++ b/packages/alfa-css/src/syntax/nth.ts @@ -92,6 +92,12 @@ export class Nth implements Iterable, Equatable, Serializable { offset: this._offset, }; } + + public toString(): string { + return this._step === 0 + ? `${this._offset}` + : `${this._step}n+${this._offset};`; + } } /** diff --git a/packages/alfa-rules/src/common/expectation/get-colors.ts b/packages/alfa-rules/src/common/expectation/get-colors.ts index d0cd458fbd..3773a6ffe3 100644 --- a/packages/alfa-rules/src/common/expectation/get-colors.ts +++ b/packages/alfa-rules/src/common/expectation/get-colors.ts @@ -134,6 +134,17 @@ function getLayers( return None; } + // If there is a background-size, we currently have no way of guessing + // whether it is large enough to go under the text or not. + // So we simply bail out. + if ( + !style + .computed("background-size") + .value.equals(style.initial("background-size").value) + ) { + return None; + } + // For each gradient, we extract all color stops into a background layer of // their own. As gradients need a start and an end point, there will always // be at least two color stops. diff --git a/packages/alfa-rules/src/common/group.ts b/packages/alfa-rules/src/common/group.ts index 700ec9bd1a..42a9bf0508 100644 --- a/packages/alfa-rules/src/common/group.ts +++ b/packages/alfa-rules/src/common/group.ts @@ -18,9 +18,9 @@ export class Group return new Group(Array.from(members)); } - private readonly _members: Array; + private readonly _members: ReadonlyArray; - private constructor(members: Array) { + private constructor(members: ReadonlyArray) { this._members = members; } diff --git a/packages/alfa-rules/src/common/normalize.ts b/packages/alfa-rules/src/common/normalize.ts new file mode 100644 index 0000000000..d6bf6a0e57 --- /dev/null +++ b/packages/alfa-rules/src/common/normalize.ts @@ -0,0 +1,3 @@ +export function normalize(input: string): string { + return input.trim().toLowerCase().replace(/\s+/g, " "); +} diff --git a/packages/alfa-rules/src/common/predicate/has-border.ts b/packages/alfa-rules/src/common/predicate/has-border.ts index 1bdbdac476..fb5aca50cc 100644 --- a/packages/alfa-rules/src/common/predicate/has-border.ts +++ b/packages/alfa-rules/src/common/predicate/has-border.ts @@ -17,9 +17,6 @@ export function hasBorder( return sides.some( (side) => style.computed(`border-${side}-width` as const).none(Length.isZero) && - style - .computed(`border-${side}-style` as const) - .none((style) => style.value === "none") && style .computed(`border-${side}-color` as const) .none((color) => color.type === "color" && Color.isTransparent(color)) diff --git a/packages/alfa-rules/src/common/predicate/has-role.ts b/packages/alfa-rules/src/common/predicate/has-role.ts index caa89d5480..17d483e872 100644 --- a/packages/alfa-rules/src/common/predicate/has-role.ts +++ b/packages/alfa-rules/src/common/predicate/has-role.ts @@ -1,15 +1,21 @@ -import { Role } from "@siteimprove/alfa-aria"; +import { Node, Role } from "@siteimprove/alfa-aria"; +import { Device } from "@siteimprove/alfa-device"; import { Element } from "@siteimprove/alfa-dom"; import { Predicate } from "@siteimprove/alfa-predicate"; -export function hasRole(predicate?: Predicate): Predicate; +export function hasRole( + device: Device, + predicate?: Predicate +): Predicate; export function hasRole( + device: Device, name: N, ...rest: Array ): Predicate; export function hasRole( + device: Device, nameOrPredicate: Predicate | Role.Name = () => true, ...names: Array ): Predicate { @@ -21,5 +27,5 @@ export function hasRole( predicate = Role.hasName(nameOrPredicate, ...names); } - return (element) => Role.from(element).some(predicate); + return (element) => Node.from(element, device).role.some(predicate); } diff --git a/packages/alfa-rules/src/common/predicate/is-tabbable.ts b/packages/alfa-rules/src/common/predicate/is-tabbable.ts index 0fd4771cf2..91833fb6ae 100644 --- a/packages/alfa-rules/src/common/predicate/is-tabbable.ts +++ b/packages/alfa-rules/src/common/predicate/is-tabbable.ts @@ -14,10 +14,10 @@ const { and, not } = Predicate; export function isTabbable(device: Device): Predicate { return and( hasTabIndex((tabIndex) => tabIndex >= 0), - and( - not(redirectsFocus), - and(not(isDisabled), not(isInert(device)), isRendered(device)) - ) + not(redirectsFocus), + not(isDisabled), + not(isInert(device)), + isRendered(device) ); } diff --git a/packages/alfa-rules/src/sia-r10/rule.ts b/packages/alfa-rules/src/sia-r10/rule.ts index cd9ae5f245..c729e8632f 100644 --- a/packages/alfa-rules/src/sia-r10/rule.ts +++ b/packages/alfa-rules/src/sia-r10/rule.ts @@ -33,7 +33,7 @@ export default Rule.Atomic.of({ hasAttribute("autocomplete", hasTokens), or( isTabbable(device), - hasRole((role) => role.isWidget()) + hasRole(device, (role) => role.isWidget()) ), isPerceivable(device), (element) => diff --git a/packages/alfa-rules/src/sia-r11/rule.ts b/packages/alfa-rules/src/sia-r11/rule.ts index dd747ca47f..0486434447 100644 --- a/packages/alfa-rules/src/sia-r11/rule.ts +++ b/packages/alfa-rules/src/sia-r11/rule.ts @@ -31,7 +31,7 @@ export default Rule.Atomic.of({ .filter( and( hasNamespace(Namespace.HTML), - hasRole("link"), + hasRole(device, "link"), not(isIgnored(device)) ) ); diff --git a/packages/alfa-rules/src/sia-r12/rule.ts b/packages/alfa-rules/src/sia-r12/rule.ts index 2370252d6a..94f44a37c5 100644 --- a/packages/alfa-rules/src/sia-r12/rule.ts +++ b/packages/alfa-rules/src/sia-r12/rule.ts @@ -27,7 +27,7 @@ export default Rule.Atomic.of({ and( not(hasInputType("image")), hasNamespace(Namespace.HTML), - hasRole("button"), + hasRole(device, "button"), not(isIgnored(device)) ) ); diff --git a/packages/alfa-rules/src/sia-r14/rule.ts b/packages/alfa-rules/src/sia-r14/rule.ts index 18463bc29e..a2a05cf620 100644 --- a/packages/alfa-rules/src/sia-r14/rule.ts +++ b/packages/alfa-rules/src/sia-r14/rule.ts @@ -8,12 +8,16 @@ import { Page } from "@siteimprove/alfa-web"; import { expectation } from "../common/expectation"; -import { hasAccessibleName } from "../common/predicate/has-accessible-name"; -import { hasAttribute } from "../common/predicate/has-attribute"; -import { hasDescendant } from "../common/predicate/has-descendant"; -import { hasRole } from "../common/predicate/has-role"; -import { isFocusable } from "../common/predicate/is-focusable"; -import { isPerceivable } from "../common/predicate/is-perceivable"; +import { normalize } from "../common/normalize"; + +import { + hasAccessibleName, + hasAttribute, + hasDescendant, + hasRole, + isFocusable, + isPerceivable, +} from "../common/predicate"; const { isElement, hasNamespace } = Element; const { isText } = Text; @@ -37,7 +41,10 @@ export default Rule.Atomic.of({ attribute.name === "aria-labelledby" ), isFocusable(device), - hasRole((role) => role.isWidget() && role.isNamedBy("contents")), + hasRole( + device, + (role) => role.isWidget() && role.isNamedBy("contents") + ), hasDescendant(and(Text.isText, isPerceivable(device)), { flattened: true, }) @@ -47,19 +54,21 @@ export default Rule.Atomic.of({ expectations(target) { const textContent = getPerceivableTextContent(target, device); + let name = ""; const accessibleNameIncludesTextContent = test( - hasAccessibleName(device, (accessibleName) => - normalize(accessibleName.value).includes(textContent) - ), + hasAccessibleName(device, (accessibleName) => { + name = normalize(accessibleName.value); + return name.includes(textContent); + }), target ); return { 1: expectation( accessibleNameIncludesTextContent, - () => Outcomes.VisibleIsInName, - () => Outcomes.VisibleIsNotInName + () => Outcomes.VisibleIsInName(textContent, name), + () => Outcomes.VisibleIsNotInName(textContent, name) ), }; }, @@ -67,10 +76,6 @@ export default Rule.Atomic.of({ }, }); -function normalize(input: string): string { - return input.trim().toLowerCase().replace(/\s+/g, " "); -} - function getPerceivableTextContent(element: Element, device: Device): string { return normalize( element @@ -83,15 +88,76 @@ function getPerceivableTextContent(element: Element, device: Device): string { } export namespace Outcomes { - export const VisibleIsInName = Ok.of( - Diagnostic.of( - `The visible text content of the element is included within its accessible name` - ) - ); + export const VisibleIsInName = (textContent: string, name: string) => + Ok.of( + LabelAndName.of( + `The visible text content of the element is included within its accessible name`, + textContent, + name + ) + ); - export const VisibleIsNotInName = Err.of( - Diagnostic.of( - `The visible text content of the element is not included within its accessible name` - ) - ); + export const VisibleIsNotInName = (textContent: string, name: string) => + Err.of( + LabelAndName.of( + `The visible text content of the element is not included within its accessible name`, + textContent, + name + ) + ); +} + +class LabelAndName extends Diagnostic { + public static of( + message: string, + textContent: string = "", + name: string = "" + ): LabelAndName { + return new LabelAndName(message, textContent, name); + } + + private readonly _textContent: string; + private readonly _name: string; + + private constructor(message: string, textContent: string, name: string) { + super(message); + this._textContent = textContent; + this._name = name; + } + + public get textContent(): string { + return this._textContent; + } + + public get name(): string { + return this._name; + } + + public equals(value: LabelAndName): boolean; + + public equals(value: unknown): value is this; + + public equals(value: unknown): boolean { + return ( + value instanceof LabelAndName && + value._message === this._message && + value._textContent === this._textContent && + value._name === this._name + ); + } + + public toJSON(): LabelAndName.JSON { + return { + ...super.toJSON(), + textContent: this._textContent, + name: this._name, + }; + } +} + +namespace LabelAndName { + export interface JSON extends Diagnostic.JSON { + textContent: string; + name: string; + } } diff --git a/packages/alfa-rules/src/sia-r15/rule.ts b/packages/alfa-rules/src/sia-r15/rule.ts index c06ab574a5..311b9ff364 100644 --- a/packages/alfa-rules/src/sia-r15/rule.ts +++ b/packages/alfa-rules/src/sia-r15/rule.ts @@ -15,8 +15,9 @@ import { hasNonEmptyAccessibleName } from "../common/predicate/has-non-empty-acc import { isIgnored } from "../common/predicate/is-ignored"; import { referenceSameResource } from "../common/predicate/reference-same-resource"; -import { Question } from "../common/question"; import { Group } from "../common/group"; +import { normalize } from "../common/normalize"; +import { Question } from "../common/question"; const { isElement, hasName, hasNamespace } = Element; const { and, not } = Predicate; @@ -107,7 +108,3 @@ export namespace Outcomes { ) ); } - -function normalize(input: string): string { - return input.trim().toLowerCase().replace(/\s+/g, " "); -} diff --git a/packages/alfa-rules/src/sia-r16/rule.ts b/packages/alfa-rules/src/sia-r16/rule.ts index 04b2496261..c8e38abfdd 100644 --- a/packages/alfa-rules/src/sia-r16/rule.ts +++ b/packages/alfa-rules/src/sia-r16/rule.ts @@ -1,13 +1,16 @@ import { Rule, Diagnostic } from "@siteimprove/alfa-act"; import { Node } from "@siteimprove/alfa-aria"; +import { Array } from "@siteimprove/alfa-array"; import { Device } from "@siteimprove/alfa-device"; import { Element, Namespace } from "@siteimprove/alfa-dom"; import { Iterable } from "@siteimprove/alfa-iterable"; import { Predicate } from "@siteimprove/alfa-predicate"; -import { Ok, Err } from "@siteimprove/alfa-result"; +import { Ok, Err, Result } from "@siteimprove/alfa-result"; import { Criterion, Technique } from "@siteimprove/alfa-wcag"; import { Page } from "@siteimprove/alfa-web"; +import * as aria from "@siteimprove/alfa-aria"; + import { expectation } from "../common/expectation"; import { hasRole } from "../common/predicate/has-role"; @@ -27,16 +30,20 @@ export default Rule.Atomic.of({ return document .descendants({ composed: true, nested: true }) .filter(isElement) - .filter(and(hasNamespace(Namespace.HTML, Namespace.SVG), hasRole())) + .filter( + and(hasNamespace(Namespace.HTML, Namespace.SVG), hasRole(device)) + ) .filter(not(isIgnored(device))); }, expectations(target) { + const diagnostic = hasRequiredValues(device, target); + return { 1: expectation( - hasRequiredValues(device)(target), - () => Outcomes.HasAllStates, - () => Outcomes.HasNotAllStates + diagnostic.isOk(), + () => Outcomes.HasAllStates(diagnostic.get()), + () => Outcomes.HasNotAllStates(diagnostic.getErr()) ), }; }, @@ -44,41 +51,147 @@ export default Rule.Atomic.of({ }, }); -function hasRequiredValues(device: Device): Predicate { - return (element) => { - const node = Node.from(element, device); - - for (const role of node.role) { - // The `separator` role is poorly architected in the sense that its - // inheritance and attribute requirements depend on aspects of the element - // carrying the role. If the element is not focusable, the `separator` - // role has no required attributes. - if (role.is("separator") && !isFocusable(device)(element)) { - return true; - } +function hasRequiredValues( + device: Device, + element: Element +): Result { + let result = true; + + const node = Node.from(element, device); + + let roleName: string = ""; + let required: Array = []; + let missing: Array = []; + + for (const role of node.role) { + roleName = role.name; + // The `separator` role is poorly architected in the sense that its + // inheritance and attribute requirements depend on aspects of the element + // carrying the role. If the element is not focusable, the `separator` + // role has no required attributes. + if (role.is("separator") && !isFocusable(device)(element)) { + return Ok.of( + RoleAndRequiredAttributes.of("", roleName, required, missing) + ); + } - for (const attribute of role.attributes) { - if ( - role.isAttributeRequired(attribute) && - node.attribute(attribute).every(property("value", isEmpty)) - ) { - return false; + for (const attribute of role.attributes) { + if (role.isAttributeRequired(attribute)) { + required.push(attribute); + + if (node.attribute(attribute).every(property("value", isEmpty))) { + missing.push(attribute); + result = false; } } } + } - return true; - }; + return result + ? Ok.of(RoleAndRequiredAttributes.of("", roleName, required, missing)) + : Err.of(RoleAndRequiredAttributes.of("", roleName, required, missing)); +} + +export class RoleAndRequiredAttributes extends Diagnostic { + public static of( + message: string, + role: string = "", + requiredAttributes: ReadonlyArray = [], + missingAttributes: ReadonlyArray = [] + ): RoleAndRequiredAttributes { + return new RoleAndRequiredAttributes( + message, + role, + requiredAttributes, + missingAttributes + ); + } + + private readonly _role: string; + private readonly _requiredAttributes: ReadonlyArray; + private readonly _missingAttributes: ReadonlyArray; + + private constructor( + message: string, + role: string, + requiredAttributes: ReadonlyArray, + missingAttributes: ReadonlyArray + ) { + super(message); + this._role = role; + this._requiredAttributes = requiredAttributes; + this._missingAttributes = missingAttributes; + } + + public get role(): string { + return this._role; + } + + public get requiredAttributes(): ReadonlyArray { + return this._requiredAttributes; + } + + public get missingAttributes(): ReadonlyArray { + return this._missingAttributes; + } + + public withMessage(message: string): RoleAndRequiredAttributes { + return new RoleAndRequiredAttributes( + message, + this._role, + this._requiredAttributes, + this._missingAttributes + ); + } + + public equals(value: RoleAndRequiredAttributes): boolean; + + public equals(value: unknown): value is this; + + public equals(value: unknown): boolean { + return ( + value instanceof RoleAndRequiredAttributes && + value._message === this._message && + value._role === this._role && + Array.equals(value._requiredAttributes, this._requiredAttributes) && + Array.equals(value._missingAttributes, this._missingAttributes) + ); + } + + public toJSON(): RoleAndRequiredAttributes.JSON { + return { + ...super.toJSON(), + role: this._role, + attributes: { + required: Array.copy(this._requiredAttributes), + missing: Array.copy(this._missingAttributes), + }, + }; + } +} + +namespace RoleAndRequiredAttributes { + export interface JSON extends Diagnostic.JSON { + role: string; + attributes: { + required: Array; + missing: Array; + }; + } } export namespace Outcomes { - export const HasAllStates = Ok.of( - Diagnostic.of(`The element has all required states and properties`) - ); - - export const HasNotAllStates = Err.of( - Diagnostic.of( - `The element does not have all required states and properties` - ) - ); + export const HasAllStates = (diagnostic: RoleAndRequiredAttributes) => + Ok.of( + diagnostic.withMessage( + `The element has all required states and properties` + ) + ); + + export const HasNotAllStates = (diagnostic: RoleAndRequiredAttributes) => + Err.of( + diagnostic.withMessage( + `The element does not have all required states and properties` + ) + ); } diff --git a/packages/alfa-rules/src/sia-r18/rule.ts b/packages/alfa-rules/src/sia-r18/rule.ts index 0642f80d62..f94d051368 100644 --- a/packages/alfa-rules/src/sia-r18/rule.ts +++ b/packages/alfa-rules/src/sia-r18/rule.ts @@ -42,7 +42,7 @@ export default Rule.Atomic.of({ 1: expectation( global.has(target.name as aria.Attribute.Name) || test( - hasRole((role) => + hasRole(device, (role) => role.isAttributeSupported(target.name as aria.Attribute.Name) ), target.owner.get() diff --git a/packages/alfa-rules/src/sia-r2/rule.ts b/packages/alfa-rules/src/sia-r2/rule.ts index 53c218fedc..059d6613e5 100644 --- a/packages/alfa-rules/src/sia-r2/rule.ts +++ b/packages/alfa-rules/src/sia-r2/rule.ts @@ -30,7 +30,7 @@ export default Rule.Atomic.of({ .filter( and( hasNamespace(Namespace.HTML), - hasRole("img"), + hasRole(device, "img"), not(isIgnored(device)) ) ); diff --git a/packages/alfa-rules/src/sia-r41/rule.ts b/packages/alfa-rules/src/sia-r41/rule.ts index e3c840ca78..18f4069a00 100644 --- a/packages/alfa-rules/src/sia-r41/rule.ts +++ b/packages/alfa-rules/src/sia-r41/rule.ts @@ -12,13 +12,16 @@ import { Page } from "@siteimprove/alfa-web"; import { expectation } from "../common/expectation"; -import { hasNonEmptyAccessibleName } from "../common/predicate/has-non-empty-accessible-name"; -import { hasRole } from "../common/predicate/has-role"; -import { isIgnored } from "../common/predicate/is-ignored"; +import { + hasNonEmptyAccessibleName, + hasRole, + isIgnored, + referenceSameResource, +} from "../common/predicate"; -import { Question } from "../common/question"; import { Group } from "../common/group"; -import { referenceSameResource } from "../common/predicate/reference-same-resource"; +import { normalize } from "../common/normalize"; +import { Question } from "../common/question"; const { isElement, hasNamespace } = Element; const { flatten } = Iterable; @@ -37,7 +40,7 @@ export default Rule.Atomic.of, Question>({ .filter( and( hasNamespace(Namespace.HTML, Namespace.SVG), - hasRole((role) => role.is("link")), + hasRole(device, (role) => role.is("link")), not(isIgnored(device)), hasNonEmptyAccessibleName(device) ) @@ -118,7 +121,3 @@ export namespace Outcomes { ) ); } - -function normalize(input: string): string { - return input.trim().toLowerCase().replace(/\s+/g, " "); -} diff --git a/packages/alfa-rules/src/sia-r42/rule.ts b/packages/alfa-rules/src/sia-r42/rule.ts index bc168427bb..2392dbf92c 100644 --- a/packages/alfa-rules/src/sia-r42/rule.ts +++ b/packages/alfa-rules/src/sia-r42/rule.ts @@ -30,7 +30,7 @@ export default Rule.Atomic.of({ and( hasNamespace(Namespace.HTML, Namespace.SVG), not(isIgnored(device)), - hasRole((role) => role.hasRequiredParent()) + hasRole(device, (role) => role.hasRequiredParent()) ) ); }, diff --git a/packages/alfa-rules/src/sia-r45/rule.ts b/packages/alfa-rules/src/sia-r45/rule.ts index 7369ad60e1..781a6d5bd2 100644 --- a/packages/alfa-rules/src/sia-r45/rule.ts +++ b/packages/alfa-rules/src/sia-r45/rule.ts @@ -27,7 +27,10 @@ export default Rule.Atomic.of({ hasNamespace(Namespace.HTML), hasName("table"), isPerceivable(device), - hasRole(not((role) => role.isPresentational())) + hasRole( + device, + not((role) => role.isPresentational()) + ) ) ) .reduce((headers, table) => { diff --git a/packages/alfa-rules/src/sia-r46/rule.ts b/packages/alfa-rules/src/sia-r46/rule.ts index 61525c2218..c9e7dde8f8 100644 --- a/packages/alfa-rules/src/sia-r46/rule.ts +++ b/packages/alfa-rules/src/sia-r46/rule.ts @@ -44,7 +44,7 @@ export default Rule.Atomic.of({ and( hasNamespace(Namespace.HTML), hasName("th"), - hasRole("rowheader", "columnheader"), + hasRole(device, "rowheader", "columnheader"), isPerceivable(device) ) ); @@ -69,7 +69,7 @@ export default Rule.Atomic.of({ table.cells.some( (cell) => // Does there exists a cell with the target as one of its headers? - hasRole((role) => role.is("cell"))(cell.element) && + hasRole(device, (role) => role.is("cell"))(cell.element) && cell.headers.some((slot) => slot.equals(header.anchor)) ), () => Outcomes.IsAssignedToDataCell, diff --git a/packages/alfa-rules/src/sia-r53/rule.ts b/packages/alfa-rules/src/sia-r53/rule.ts index 0c055ecd04..016fdde303 100644 --- a/packages/alfa-rules/src/sia-r53/rule.ts +++ b/packages/alfa-rules/src/sia-r53/rule.ts @@ -19,7 +19,7 @@ export default Rule.Atomic.of({ const headings = document .descendants({ flattened: true }) .filter(isElement) - .filter(hasRole("heading")) + .filter(hasRole(device, "heading")) .reject(isIgnored(device)); return { diff --git a/packages/alfa-rules/src/sia-r56/rule.ts b/packages/alfa-rules/src/sia-r56/rule.ts new file mode 100644 index 0000000000..f986e998a1 --- /dev/null +++ b/packages/alfa-rules/src/sia-r56/rule.ts @@ -0,0 +1,166 @@ +import { Diagnostic, Rule } from "@siteimprove/alfa-act"; +import { Node, Role } from "@siteimprove/alfa-aria"; +import { Array } from "@siteimprove/alfa-array"; +import { Element, Namespace } from "@siteimprove/alfa-dom"; +import { Iterable } from "@siteimprove/alfa-iterable"; +import { List } from "@siteimprove/alfa-list"; +import { Map } from "@siteimprove/alfa-map"; +import { Option } from "@siteimprove/alfa-option"; +import { Predicate } from "@siteimprove/alfa-predicate"; +import { Err, Ok } from "@siteimprove/alfa-result"; +import { Page } from "@siteimprove/alfa-web"; + +import { expectation } from "../common/expectation"; +import { Group } from "../common/group"; +import { normalize } from "../common/normalize"; + +import { hasRole, isIgnored } from "../common/predicate"; + +const { and, equals, not } = Predicate; +const { hasNamespace } = Element; + +export default Rule.Atomic.of>({ + uri: "https://siteimprove.github.io/sanshikan/rules/sia-r56.html", + evaluate({ device, document }) { + return { + applicability() { + return document + .descendants({ flattened: true, nested: true }) + .filter(Element.isElement) + .filter( + and( + hasNamespace(equals(Namespace.HTML)), + not(isIgnored(device)), + hasRole(device, (role) => role.is("landmark")) + ) + ) + .reduce((groups, landmark) => { + // Since we already have filtered by having a landmark role, we can + // safely get the role. + const role = Node.from(landmark, device).role.get(); + + groups = groups.set( + role, + groups + .get(role) + .getOrElse(() => List.empty()) + .append(landmark) + ); + + return groups; + }, Map.empty>()) + .filter((elements) => elements.size > 1) + .map(Group.of) + .values(); + }, + + expectations(target) { + // Empty groups have been filtered out already, so we can safely get the + // first element + const role = Node.from( + Iterable.first(target).get()!, + device + ).role.get().name; + + const byNames = [...target] + .reduce((groups, landmark) => { + const name = Node.from(landmark, device).name.map((name) => + normalize(name.value) + ); + groups = groups.set( + name, + groups + .get(name) + .getOrElse(() => List.empty()) + .append(landmark) + ); + + return groups; + }, Map.empty, List>()) + .filter((landmarks) => landmarks.size > 1); + + return { + 1: expectation( + byNames.size === 0, + () => Outcomes.differentNames(role), + () => Outcomes.sameNames(role, byNames.values()) + ), + }; + }, + }; + }, +}); + +export namespace Outcomes { + export const differentNames = (role: Role.Name) => + Ok.of(Diagnostic.of(`No two \`${role}\` have the same name.`)); + + export const sameNames = ( + role: Role.Name, + errors: Iterable> + ) => + Err.of(SameNames.of(`Some \`${role}\` have the same name.`, role, errors)); +} + +class SameNames extends Diagnostic implements Iterable> { + public static of( + message: string, + role: Role.Name = "none", + errors: Iterable> = [] + ): SameNames { + return new SameNames(message, role, Array.from(errors).map(List.from)); + } + + private readonly _role: Role.Name; + private readonly _errors: ReadonlyArray>; + + private constructor( + message: string, + role: Role.Name, + errors: ReadonlyArray> + ) { + super(message); + this._role = role; + this._errors = errors; + } + + public get role(): Role.Name { + return this._role; + } + + public *[Symbol.iterator](): Iterator> { + yield* this._errors; + } + + public equals(value: SameNames): boolean; + + public equals(value: unknown): value is this; + + public equals(value: unknown): boolean { + return ( + value instanceof SameNames && + value._message === this._message && + value._role === this._role && + value._errors.every((list, idx) => list.equals(this._errors[idx])) + ); + } + + public toJSON(): SameNames.JSON { + return { + ...super.toJSON(), + role: this._role, + errors: Array.toJSON(this._errors), + }; + } +} + +namespace SameNames { + export interface JSON extends Diagnostic.JSON { + role: string; + errors: Array>; + } + + export function isSameNames(value: unknown): value is SameNames { + return value instanceof SameNames; + } +} diff --git a/packages/alfa-rules/src/sia-r57/rule.ts b/packages/alfa-rules/src/sia-r57/rule.ts index 0cbeadfaff..6f651eae33 100644 --- a/packages/alfa-rules/src/sia-r57/rule.ts +++ b/packages/alfa-rules/src/sia-r57/rule.ts @@ -30,7 +30,7 @@ export default Rule.Atomic.of({ if ( descendants .filter(isElement) - .some(hasRole((role) => role.isLandmark())) + .some(hasRole(device, (role) => role.isLandmark())) ) { yield* descendants .filter(isText) diff --git a/packages/alfa-rules/src/sia-r59/rule.ts b/packages/alfa-rules/src/sia-r59/rule.ts index 1c6c987e2c..6ab590cb5a 100644 --- a/packages/alfa-rules/src/sia-r59/rule.ts +++ b/packages/alfa-rules/src/sia-r59/rule.ts @@ -15,7 +15,7 @@ const { and, test } = Predicate; export default Rule.Atomic.of({ uri: "https://alfa.siteimprove.com/rules/sia-r59", - evaluate({ document }) { + evaluate({ device, document }) { return { applicability() { return test(hasChild(isDocumentElement), document) ? [document] : []; @@ -25,7 +25,7 @@ export default Rule.Atomic.of({ const hasHeadings = target .descendants({ flattened: true }) .filter(isElement) - .some(and(hasNamespace(Namespace.HTML), hasRole("heading"))); + .some(and(hasNamespace(Namespace.HTML), hasRole(device, "heading"))); return { 1: expectation( diff --git a/packages/alfa-rules/src/sia-r61/rule.ts b/packages/alfa-rules/src/sia-r61/rule.ts index 6d1ac21100..213b4c5445 100644 --- a/packages/alfa-rules/src/sia-r61/rule.ts +++ b/packages/alfa-rules/src/sia-r61/rule.ts @@ -23,7 +23,7 @@ export default Rule.Atomic.of({ const firstHeading = document .descendants({ flattened: true }) .filter(and(isElement, not(isIgnored(device)))) - .find(hasRole("heading")); + .find(hasRole(device, "heading")); return { applicability() { diff --git a/packages/alfa-rules/src/sia-r62/rule.ts b/packages/alfa-rules/src/sia-r62/rule.ts index f79f4acc36..3f99452913 100644 --- a/packages/alfa-rules/src/sia-r62/rule.ts +++ b/packages/alfa-rules/src/sia-r62/rule.ts @@ -2,13 +2,19 @@ import { Rule, Diagnostic } from "@siteimprove/alfa-act"; 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 { 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 { Err, Ok } from "@siteimprove/alfa-result"; +import { Err, Ok, Result } from "@siteimprove/alfa-result"; import { Context } from "@siteimprove/alfa-selector"; -import { Style } from "@siteimprove/alfa-style"; +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/expectation"; import { @@ -19,35 +25,39 @@ import { isVisible, } from "../common/predicate"; -import { Question } from "../common/question"; - const { isElement } = Element; const { isText } = Text; const { and, or, not, test } = Predicate; -export default Rule.Atomic.of({ +export default Rule.Atomic.of({ uri: "https://alfa.siteimprove.com/rules/sia-r62", requirements: [Criterion.of("1.4.1")], evaluate({ device, document }) { + let containers: Map = Map.empty(); + return { applicability() { - return visit(document, false); + return visit(document, None); - function* visit(node: Node, collect: boolean): Iterable { + function* visit( + node: Node, + container: Option + ): Iterable { if (isElement(node)) { // If the element is a semantic link, it might be applicable. if ( test( - hasRole((role) => role.is("link")), + hasRole(device, (role) => role.is("link")), node ) ) { if ( - collect && + container.isSome() && node .descendants({ flattened: true }) .some(and(isText, isVisible(device))) ) { + containers = containers.set(node, container.get()); return yield node; } } @@ -55,9 +65,12 @@ export default Rule.Atomic.of({ // Otherwise, if the element is a

element with non-link text // content then start collecting applicable elements. else if ( - test(and(hasRole("paragraph"), hasNonLinkText(device)), node) + test( + and(hasRole(device, "paragraph"), hasNonLinkText(device)), + node + ) ) { - collect = true; + container = Option.of(node); } } @@ -67,53 +80,39 @@ export default Rule.Atomic.of({ }); for (const child of children) { - yield* visit(child, collect); + yield* visit(child, container); } } }, expectations(target) { - const container = target - .ancestors({ - flattened: true, - }) - .filter(isElement) - .find(hasRole("paragraph")) - .get(); + const container = containers.get(target).get(); + + const defaultStyle = isDistinguishable(target, container, device); + const hoverStyle = isDistinguishable( + target, + container, + device, + Context.hover(target) + ); + const focusStyle = isDistinguishable( + target, + container, + device, + Context.focus(target) + ); return { 1: expectation( - test( - and( - isDistinguishable(container, device), - isDistinguishable(container, device, Context.hover(target)), - isDistinguishable(container, device, Context.focus(target)) - ), - target - ), - () => Outcomes.IsDistinguishable, - () => Outcomes.IsNotDistinguishable - ), - - 2: expectation( - test( - and( - isDistinguishable(container, device, Context.visit(target)), - isDistinguishable( - container, - device, - Context.hover(target).visit(target) - ), - isDistinguishable( - container, - device, - Context.focus(target).visit(target) - ) - ), - target - ), - () => Outcomes.IsDistinguishableWhenVisited, - () => Outcomes.IsNotDistinguishableWhenVisited + defaultStyle.isOk() && hoverStyle.isOk() && focusStyle.isOk(), + () => + Outcomes.IsDistinguishable(defaultStyle, hoverStyle, focusStyle), + () => + Outcomes.IsNotDistinguishable( + defaultStyle, + hoverStyle, + focusStyle + ) ), }; }, @@ -122,29 +121,37 @@ export default Rule.Atomic.of({ }); export namespace Outcomes { - export const IsDistinguishable = Ok.of( - Diagnostic.of(`The link is distinguishable from the surrounding text`) - ); - - export const IsNotDistinguishable = Err.of( - Diagnostic.of( - `The link is not distinguishable from the surrounding text, either in its - default state, or on hover and focus` - ) - ); - - export const IsDistinguishableWhenVisited = Ok.of( - Diagnostic.of( - `When visited, the link is distinguishable from the surrounding text` - ) - ); - - export const IsNotDistinguishableWhenVisited = Err.of( - Diagnostic.of( - `When visited, the link is not distinguishable from the surrounding text, - either in its default state, or on hover and focus` - ) - ); + // 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 = ( + defaultStyle: Result, + hoverStyle: Result, + focusStyle: Result + ) => + Ok.of( + DistinguishingStyles.of( + `The link is distinguishable from the surrounding text`, + defaultStyle, + hoverStyle, + focusStyle + ) + ); + + export const IsNotDistinguishable = ( + defaultStyle: Result, + hoverStyle: Result, + focusStyle: Result + ) => + Err.of( + DistinguishingStyles.of( + `The link is not distinguishable from the surrounding text`, + defaultStyle, + hoverStyle, + focusStyle + ) + ); } function hasNonLinkText(device: Device): Predicate { @@ -159,31 +166,41 @@ function hasNonLinkText(device: Device): Predicate { return children .filter(isElement) - .reject(hasRole((role) => role.is("link"))) + .reject(hasRole(device, (role) => role.is("link"))) .some(hasNonLinkText); }; } function isDistinguishable( + target: Element, container: Element, device: Device, - context?: Context -): 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), - - // 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) - ); + context: Context = Context.empty() +): Result { + const style = ComputedStyles.from(target, device, context); + + return test( + 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), + + // 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) + ), + target + ) + ? Ok.of(style) + : Err.of(style); } function hasDistinguishableTextDecoration( @@ -223,3 +240,222 @@ function hasDistinguishableBackground( .none((color) => Color.isTransparent(color) || color.equals(reference)); }; } + +/** + * 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 (element) => { + return Style.from(element, device, context) + .computed("font-weight") + .none((weight) => weight.equals(reference)); + }; +} + +type Name = Property.Name | Property.Shorthand.Name; + +export class ComputedStyles implements Equatable, 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 toJSON(): ComputedStyles.JSON { + return { + style: this._style.toJSON(), + }; + } +} + +export namespace ComputedStyles { + export interface JSON { + [key: string]: json.JSON; + style: Map.JSON; + } + + export function from( + element: Element, + device: Device, + context: Context = Context.empty() + ): ComputedStyles { + const style = Style.from(element, device, context); + + // Trying to reduce the footprint of the result by exporting shorthands + // rather than longhands, and avoiding to export values that are the same + // as the initial value of the property. + function fourValuesShorthand( + postfix: "color" | "style" | "width" + ): readonly [Name, string] { + const shorthand = `border-${postfix}` as const; + + function getLongHand(side: "top" | "right" | "bottom" | "left"): string { + return style.computed(`border-${side}-${postfix}` as const).toString(); + } + + let top = getLongHand("top"); + let right = getLongHand("right"); + let bottom = getLongHand("bottom"); + let left = getLongHand("left"); + + if (left === right) { + left = ""; + if (bottom === top) { + bottom = ""; + if (right === top) { + right = ""; + if ( + top === + Property.get(`border-top-${postfix}` as const).initial.toString() + ) { + top = ""; + } + } + } + } + + return [shorthand, `${top} ${right} ${bottom} ${left}`.trim()]; + } + + const shorthands = (["color", "style", "width"] as const).map((postfix) => + fourValuesShorthand(postfix) + ); + + function longhand(name: Property.Name): string { + const property = style.computed(name).toString(); + + return property === Property.get(name).initial.toString() ? "" : property; + } + + const outline = `${longhand("outline-color")} ${longhand( + "outline-style" + )} ${longhand("outline-width")}`.trim(); + + // While text-decoration-style and text-decoration-thickness are not + // important for deciding if there is one, but they are important for + // rendering the link with the correct styling. + const textDecoration = `${longhand("text-decoration-line")} ${longhand( + "text-decoration-color" + )} ${longhand("text-decoration-style")} ${longhand( + "text-decoration-thickness" + )}`.trim(); + + const longhands = ([ + "background-color", + "color", + "font-weight", + ] as const).map((property) => [property, longhand(property)] as const); + + return ComputedStyles.of( + [ + ...shorthands, + ...longhands, + ["outline", outline] as const, + ["text-decoration", textDecoration] as const, + ].filter(([_, value]) => value !== "") + ); + } +} + +export class DistinguishingStyles extends Diagnostic { + public static of( + message: string, + defaultStyle: Result = Err.of(ComputedStyles.of([])), + hoverStyle: Result = Err.of(ComputedStyles.of([])), + focusStyle: Result = Err.of(ComputedStyles.of([])) + ): DistinguishingStyles { + return new DistinguishingStyles( + message, + defaultStyle, + hoverStyle, + focusStyle + ); + } + + private readonly _defaultStyle: Result; + private readonly _hoverStyle: Result; + private readonly _focusStyle: Result; + + private constructor( + message: string, + defaultStyle: Result, + hoverStyle: Result, + focusStyle: Result + ) { + super(message); + this._defaultStyle = defaultStyle; + this._hoverStyle = hoverStyle; + this._focusStyle = focusStyle; + } + + public get defaultStyle(): Result { + return this._defaultStyle; + } + + public get hoverStyle(): Result { + return this._hoverStyle; + } + + public get focusStyle(): Result { + return this._focusStyle; + } + + public equals(value: DistinguishingStyles): boolean; + + public equals(value: unknown): value is this; + + public equals(value: unknown): boolean { + return ( + value instanceof DistinguishingStyles && + value._defaultStyle.equals(this._defaultStyle) && + value._hoverStyle.equals(this._hoverStyle) && + value._focusStyle.equals(this._focusStyle) + ); + } + + public toJSON(): DistinguishingStyles.JSON { + return { + ...super.toJSON(), + defaultStyle: this._defaultStyle.toJSON(), + hoverStyle: this._hoverStyle.toJSON(), + focusStyle: this._focusStyle.toJSON(), + }; + } +} + +export namespace DistinguishingStyles { + export interface JSON extends Diagnostic.JSON { + defaultStyle: Result.JSON; + hoverStyle: Result.JSON; + focusStyle: Result.JSON; + } +} diff --git a/packages/alfa-rules/src/sia-r64/rule.ts b/packages/alfa-rules/src/sia-r64/rule.ts index febc55c880..6cf1ea07ec 100644 --- a/packages/alfa-rules/src/sia-r64/rule.ts +++ b/packages/alfa-rules/src/sia-r64/rule.ts @@ -30,7 +30,7 @@ export default Rule.Atomic.of({ .filter( and( hasNamespace(Namespace.HTML), - hasRole("heading"), + hasRole(device, "heading"), not(isIgnored(device)) ) ); diff --git a/packages/alfa-rules/src/sia-r65/rule.ts b/packages/alfa-rules/src/sia-r65/rule.ts index 2298b858f8..aaf0feccfb 100644 --- a/packages/alfa-rules/src/sia-r65/rule.ts +++ b/packages/alfa-rules/src/sia-r65/rule.ts @@ -12,6 +12,7 @@ import { Page } from "@siteimprove/alfa-web"; import { expectation } from "../common/expectation"; import { + hasBorder, hasBoxShadow, hasOutline, hasTextDecoration, @@ -22,7 +23,7 @@ import { Question } from "../common/question"; const { isElement } = Element; const { isKeyword } = Keyword; -const { or, xor } = Predicate; +const { or, test, xor } = Predicate; export default Rule.Atomic.of({ uri: "https://alfa.siteimprove.com/rules/sia-r65", @@ -99,7 +100,9 @@ function hasFocusIndicator(device: Device): Predicate { xor(hasBoxShadow(device), hasBoxShadow(device, withFocus)), // These properties are essentially always set, so any difference in the color is good enough. hasDifferentColors(device, withFocus), - hasDifferentBackgroundColors(device, withFocus) + hasDifferentBackgroundColors(device, withFocus), + // Any difference in border is accepted + hasDifferentBorder(device, withFocus) ) ); }; @@ -148,3 +151,55 @@ function hasDifferentBackgroundColors( return !color1.equals(color2); }; } + +function hasDifferentBorder( + device: Device, + context1: Context = Context.empty(), + context2: Context = Context.empty() +): Predicate { + return function hasDifferentBorder(element: Element): boolean { + const style1 = Style.from(element, device, context1); + const style2 = Style.from(element, device, context2); + + // If 0 or 1 has border, the answer is easy. + const hasBorder1 = test(hasBorder(device, context1), element); + const hasBorder2 = test(hasBorder(device, context2), element); + + if (hasBorder1 !== hasBorder2) { + // only one has border + return true; + } + + if (!hasBorder1 && !hasBorder2) { + // none has border + return false; + } + + // They both have border, we need to dig the values + + // We consider any difference in any of the border-* longhand as enough + for (const side of ["top", "right", "bottom", "left"] as const) { + for (const effect of ["color", "style", "width"] as const) { + const longhand = `border-${side}-${effect}` as const; + + const border1 = style1.computed(longhand); + const border2 = style2.computed(longhand); + + // We avoid keyword resolution for color, + // but we need it for style. The none=hidden conflict has been solved + // by hasBorder so any difference in style is enough. + if ( + !( + (effect === "color" && + (isKeyword(border1) || isKeyword(border2))) || + border1.equals(border2) + ) + ) { + return true; + } + } + } + + return false; + }; +} diff --git a/packages/alfa-rules/src/sia-r66/rule.ts b/packages/alfa-rules/src/sia-r66/rule.ts index 63f005c0c5..1d63bb6faa 100644 --- a/packages/alfa-rules/src/sia-r66/rule.ts +++ b/packages/alfa-rules/src/sia-r66/rule.ts @@ -46,8 +46,8 @@ export default Rule.Atomic.of({ isElement, or( not(Element.hasNamespace(Namespace.HTML)), - hasRole((role) => role.isWidget()), - and(hasRole("group"), isSemanticallyDisabled) + hasRole(device, (role) => role.isWidget()), + and(hasRole(device, "group"), isSemanticallyDisabled) ) ), node diff --git a/packages/alfa-rules/src/sia-r68/rule.ts b/packages/alfa-rules/src/sia-r68/rule.ts index 5986192ff6..56e33a261a 100644 --- a/packages/alfa-rules/src/sia-r68/rule.ts +++ b/packages/alfa-rules/src/sia-r68/rule.ts @@ -113,7 +113,7 @@ function* visit(node: Node, device: Device): Iterable { and( hasNamespace(Namespace.HTML, Namespace.SVG), not(isIgnored(device)), - hasRole((role) => role.hasRequiredChildren()) + hasRole(device, (role) => role.hasRequiredChildren()) ) )(node) ) { diff --git a/packages/alfa-rules/src/sia-r69/rule.ts b/packages/alfa-rules/src/sia-r69/rule.ts index d942df7284..3e438140fa 100644 --- a/packages/alfa-rules/src/sia-r69/rule.ts +++ b/packages/alfa-rules/src/sia-r69/rule.ts @@ -46,8 +46,8 @@ export default Rule.Atomic.of({ isElement, or( not(Element.hasNamespace(Namespace.HTML)), - hasRole((role) => role.isWidget()), - and(hasRole("group"), isSemanticallyDisabled) + hasRole(device, (role) => role.isWidget()), + and(hasRole(device, "group"), isSemanticallyDisabled) ) ), node diff --git a/packages/alfa-rules/src/sia-r71/rule.ts b/packages/alfa-rules/src/sia-r71/rule.ts index d2033fcae0..7fdafe276c 100644 --- a/packages/alfa-rules/src/sia-r71/rule.ts +++ b/packages/alfa-rules/src/sia-r71/rule.ts @@ -25,7 +25,7 @@ export default Rule.Atomic.of({ nested: true, }) .filter(isElement) - .filter(and(hasRole("paragraph"), isVisible(device))); + .filter(and(hasRole(device, "paragraph"), isVisible(device))); }, expectations(target) { diff --git a/packages/alfa-rules/src/sia-r72/rule.ts b/packages/alfa-rules/src/sia-r72/rule.ts index e593920366..86b3fcf9ab 100644 --- a/packages/alfa-rules/src/sia-r72/rule.ts +++ b/packages/alfa-rules/src/sia-r72/rule.ts @@ -23,7 +23,7 @@ export default Rule.Atomic.of({ nested: true, }) .filter(isElement) - .filter(and(hasRole("paragraph"), isVisible(device))); + .filter(and(hasRole(device, "paragraph"), isVisible(device))); }, expectations(target) { diff --git a/packages/alfa-rules/src/sia-r73/rule.ts b/packages/alfa-rules/src/sia-r73/rule.ts index f8e106029b..5a3ddad88d 100644 --- a/packages/alfa-rules/src/sia-r73/rule.ts +++ b/packages/alfa-rules/src/sia-r73/rule.ts @@ -24,7 +24,7 @@ export default Rule.Atomic.of({ nested: true, }) .filter(isElement) - .filter(and(hasRole("paragraph"), isVisible(device))); + .filter(and(hasRole(device, "paragraph"), isVisible(device))); }, expectations(target) { diff --git a/packages/alfa-rules/src/sia-r74/rule.ts b/packages/alfa-rules/src/sia-r74/rule.ts index 6764503ab2..2e1dcd39e5 100644 --- a/packages/alfa-rules/src/sia-r74/rule.ts +++ b/packages/alfa-rules/src/sia-r74/rule.ts @@ -28,7 +28,7 @@ export default Rule.Atomic.of({ .filter(isElement) .filter( and( - hasRole("paragraph"), + hasRole(device, "paragraph"), (element) => Style.from(element, device) .cascaded("font-size") diff --git a/packages/alfa-rules/src/sia-r8/rule.ts b/packages/alfa-rules/src/sia-r8/rule.ts index 7fff097b56..8957d6b91f 100644 --- a/packages/alfa-rules/src/sia-r8/rule.ts +++ b/packages/alfa-rules/src/sia-r8/rule.ts @@ -27,6 +27,7 @@ export default Rule.Atomic.of({ and( hasNamespace(Namespace.HTML), hasRole( + device, "checkbox", "combobox", "listbox", diff --git a/packages/alfa-rules/src/sia-r80/rule.ts b/packages/alfa-rules/src/sia-r80/rule.ts index 897c24f4bd..3ee268f86f 100644 --- a/packages/alfa-rules/src/sia-r80/rule.ts +++ b/packages/alfa-rules/src/sia-r80/rule.ts @@ -28,7 +28,7 @@ export default Rule.Atomic.of({ .filter(isElement) .filter( and( - hasRole("paragraph"), + hasRole(device, "paragraph"), (element) => Style.from(element, device).cascaded("line-height").isSome(), hasTextContent(), diff --git a/packages/alfa-rules/src/sia-r81/rule.ts b/packages/alfa-rules/src/sia-r81/rule.ts index 9a055a3f08..e39315aef2 100644 --- a/packages/alfa-rules/src/sia-r81/rule.ts +++ b/packages/alfa-rules/src/sia-r81/rule.ts @@ -1,5 +1,6 @@ import { Rule, Diagnostic } from "@siteimprove/alfa-act"; import { Node } from "@siteimprove/alfa-aria"; +import { Device } from "@siteimprove/alfa-device"; import { Element, Namespace } from "@siteimprove/alfa-dom"; import { Iterable } from "@siteimprove/alfa-iterable"; import { List } from "@siteimprove/alfa-list"; @@ -15,13 +16,16 @@ import * as dom from "@siteimprove/alfa-dom"; import { expectation } from "../common/expectation"; -import { hasNonEmptyAccessibleName } from "../common/predicate/has-non-empty-accessible-name"; -import { hasRole } from "../common/predicate/has-role"; -import { isIgnored } from "../common/predicate/is-ignored"; +import { + hasNonEmptyAccessibleName, + hasRole, + isIgnored, + referenceSameResource, +} from "../common/predicate"; -import { Question } from "../common/question"; import { Group } from "../common/group"; -import { referenceSameResource } from "../common/predicate/reference-same-resource"; +import { normalize } from "../common/normalize"; +import { Question } from "../common/question"; const { isElement, hasName, hasNamespace, hasId } = Element; const { flatten } = Iterable; @@ -40,12 +44,14 @@ export default Rule.Atomic.of, Question>({ .filter( and( hasNamespace(Namespace.HTML, Namespace.SVG), - hasRole((role) => role.is("link")), + hasRole(device, (role) => role.is("link")), not(isIgnored(device)), hasNonEmptyAccessibleName(device) ) ) - .groupBy((element) => linkContext(element).add(element.root())) + .groupBy((element) => + linkContext(element, device).add(element.root()) + ) .map((elements) => elements .reduce((groups, element) => { @@ -121,22 +127,18 @@ export namespace Outcomes { ); } -function normalize(input: string): string { - return input.trim().toLowerCase().replace(/\s+/g, " "); -} - /** * @todo For links in table cells, account for the text in the associated table * header cell. * * {@link https://www.w3.org/TR/WCAG/#dfn-programmatically-determined-link-context} */ -function linkContext(element: Element): Set { +function linkContext(element: Element, device: Device): Set { let context = Set.empty(); const ancestors = element.ancestors({ flattened: true }).filter(isElement); - for (const listitem of ancestors.filter(hasRole("listitem"))) { + for (const listitem of ancestors.filter(hasRole(device, "listitem"))) { context = context.add(listitem); } @@ -144,7 +146,7 @@ function linkContext(element: Element): Set { context = context.add(paragraph); } - for (const cell of ancestors.find(hasRole("cell", "gridcell"))) { + for (const cell of ancestors.find(hasRole(device, "cell", "gridcell"))) { context = context.add(cell); } diff --git a/packages/alfa-rules/src/sia-r82/rule.ts b/packages/alfa-rules/src/sia-r82/rule.ts index 70dadd882c..adca18c75c 100644 --- a/packages/alfa-rules/src/sia-r82/rule.ts +++ b/packages/alfa-rules/src/sia-r82/rule.ts @@ -30,6 +30,7 @@ export default Rule.Atomic.of({ and( hasNamespace(Namespace.HTML), hasRole( + device, "checkbox", "combobox", "listbox", diff --git a/packages/alfa-rules/src/sia-r85/rule.ts b/packages/alfa-rules/src/sia-r85/rule.ts index 849998d12f..e3bcae9192 100644 --- a/packages/alfa-rules/src/sia-r85/rule.ts +++ b/packages/alfa-rules/src/sia-r85/rule.ts @@ -23,7 +23,7 @@ export default Rule.Atomic.of({ nested: true, }) .filter(isElement) - .filter(and(hasRole("paragraph"), isVisible(device))); + .filter(and(hasRole(device, "paragraph"), isVisible(device))); }, expectations(target) { diff --git a/packages/alfa-rules/src/sia-r87/rule.ts b/packages/alfa-rules/src/sia-r87/rule.ts index 2f00ef0991..8fbd0800ba 100644 --- a/packages/alfa-rules/src/sia-r87/rule.ts +++ b/packages/alfa-rules/src/sia-r87/rule.ts @@ -20,7 +20,7 @@ import { Question } from "../common/question"; import { isAtTheStart } from "../common/predicate/is-at-the-start"; const { hasName, isElement } = Element; -const { not, fold } = Predicate; +const { fold } = Predicate; const { and } = Refinement; export default Rule.Atomic.of({ @@ -67,7 +67,7 @@ export default Rule.Atomic.of({ // there can be more than one element with a role of main, going to any of these is OK. const mains = document .inclusiveDescendants({ flattened: true }) - .filter(and(isElement, hasRole("main"))); + .filter(and(isElement, hasRole(device, "main"))); const askIsMain = Question.of( "first-tabbable-reference-is-main", @@ -96,12 +96,12 @@ export default Rule.Atomic.of({ () => Outcomes.HasNoTabbable, () => expectation( - element.none(hasRole((role) => role.is("link"))), - () => Outcomes.FirstTabbableIsNotLink, + element.some(isIgnored(device)), + () => Outcomes.FirstTabbableIsIgnored, () => expectation( - element.none(not(isIgnored(device))), - () => Outcomes.FirstTabbableIsIgnored, + element.none(hasRole(device, (role) => role.is("link"))), + () => Outcomes.FirstTabbableIsNotLink, () => expectation( element.none(isKeyboardActionable(device)), @@ -133,7 +133,7 @@ export default Rule.Atomic.of({ expectation( reference .filter(isElement) - .some(hasRole("main")), + .some(hasRole(device, "main")), () => Outcomes.FirstTabbableIsLinkToContent, () => diff --git a/packages/alfa-rules/src/sia-r90/rule.ts b/packages/alfa-rules/src/sia-r90/rule.ts index 9c163f6786..36e33b4fbb 100644 --- a/packages/alfa-rules/src/sia-r90/rule.ts +++ b/packages/alfa-rules/src/sia-r90/rule.ts @@ -25,7 +25,7 @@ export default Rule.Atomic.of({ isElement, and( hasNamespace(Namespace.HTML, Namespace.SVG), - hasRole((role) => role.hasPresentationalChildren()) + hasRole(device, (role) => role.hasPresentationalChildren()) ) ) ); diff --git a/packages/alfa-rules/src/sia-r94/rule.ts b/packages/alfa-rules/src/sia-r94/rule.ts index da9fa1f8cf..0584bac454 100644 --- a/packages/alfa-rules/src/sia-r94/rule.ts +++ b/packages/alfa-rules/src/sia-r94/rule.ts @@ -26,7 +26,7 @@ export default Rule.Atomic.of({ .filter( and( hasNamespace(Namespace.HTML), - hasRole("menuitem"), + hasRole(device, "menuitem"), not(isIgnored(device)) ) ); diff --git a/packages/alfa-rules/test/common/predicate/has-role.spec.tsx b/packages/alfa-rules/test/common/predicate/has-role.spec.tsx new file mode 100644 index 0000000000..57755e3dba --- /dev/null +++ b/packages/alfa-rules/test/common/predicate/has-role.spec.tsx @@ -0,0 +1,16 @@ +import { h } from "@siteimprove/alfa-dom/h"; +import { test } from "@siteimprove/alfa-test"; + +import { Device } from "@siteimprove/alfa-device"; +import { hasRole } from "../../../src/common/predicate/has-role"; + +const device = Device.standard(); + +test(`hasRole() respects presentational children`, (t) => { + const target = Foo; + const tab =

{target}
; + + h.document([tab]); + + t.deepEqual(hasRole(device, "link")(target), false); +}); diff --git a/packages/alfa-rules/test/sia-r1/rule.spec.tsx b/packages/alfa-rules/test/sia-r1/rule.spec.tsx index 015a546162..94209cca20 100644 --- a/packages/alfa-rules/test/sia-r1/rule.spec.tsx +++ b/packages/alfa-rules/test/sia-r1/rule.spec.tsx @@ -1,14 +1,13 @@ +import { h } from "@siteimprove/alfa-dom"; import { test } from "@siteimprove/alfa-test"; -import { Document } from "@siteimprove/alfa-dom"; - import R1, { Outcomes } from "../../src/sia-r1/rule"; import { evaluate } from "../common/evaluate"; import { passed, failed, inapplicable } from "../common/outcome"; test("evaluate() passes a document that that a non-empty element", async (t) => { - const document = Document.of([ + const document = h.document([ <html> <head> <title>Hello world @@ -25,7 +24,7 @@ test("evaluate() passes a document that that a non-empty element", async }); test("evaluate() fails a document that has no <title> element", async (t) => { - const document = Document.of([<html />]); + const document = h.document([<html />]); t.deepEqual(await evaluate(R1, { document }), [ failed(R1, document, { 1: Outcomes.HasNoTitle, 2: Outcomes.HasEmptyTitle }), @@ -33,7 +32,7 @@ test("evaluate() fails a document that has no <title> element", async (t) => { }); test("evaluate() fails a document that has an empty <title> element", async (t) => { - const document = Document.of([ + const document = h.document([ <html> <head> <title /> @@ -47,7 +46,7 @@ test("evaluate() fails a document that has an empty <title> element", async (t) }); test("evaluate() is inapplicable to a document that is not an HTML document", async (t) => { - const document = Document.empty(); + const document = h.document([]); t.deepEqual(await evaluate(R1, { document }), [inapplicable(R1)]); }); diff --git a/packages/alfa-rules/test/sia-r10/rule.spec.tsx b/packages/alfa-rules/test/sia-r10/rule.spec.tsx index a61896d33f..3c7d08e84a 100644 --- a/packages/alfa-rules/test/sia-r10/rule.spec.tsx +++ b/packages/alfa-rules/test/sia-r10/rule.spec.tsx @@ -1,7 +1,6 @@ +import { h } from "@siteimprove/alfa-dom"; import { test } from "@siteimprove/alfa-test"; -import { Document } from "@siteimprove/alfa-dom"; - import R10, { Outcomes } from "../../src/sia-r10/rule"; import { evaluate } from "../common/evaluate"; @@ -11,7 +10,7 @@ test("evaluate() passes a valid simple autocomplete attribute on an <input> elem const element = <input autocomplete="username" />; const target = element.attribute("autocomplete").get()!; - const document = Document.of([element]); + const document = h.document([element]); t.deepEqual(await evaluate(R10, { document }), [ passed(R10, target, { @@ -26,7 +25,7 @@ test("evaluate() passes a valid complex autocomplete attribute on an <input> ele ); const target = element.attribute("autocomplete").get()!; - const document = Document.of([element]); + const document = h.document([element]); t.deepEqual(await evaluate(R10, { document }), [ passed(R10, target, { @@ -39,7 +38,7 @@ test("evaluate() fails an autocomplete attribute with a non-existing term", asyn const element = <input autocomplete="invalid" />; const target = element.attribute("autocomplete").get()!; - const document = Document.of([element]); + const document = h.document([element]); t.deepEqual(await evaluate(R10, { document }), [ failed(R10, target, { @@ -52,7 +51,7 @@ test("evaluate() fails an autocomplete attribute with terms in wrong order", asy const element = <input autocomplete="work shipping email" />; const target = element.attribute("autocomplete").get()!; - const document = Document.of([element]); + const document = h.document([element]); t.deepEqual(await evaluate(R10, { document }), [ failed(R10, target, { @@ -65,7 +64,7 @@ test("evaluate() fails an autocomplete attribute with an inappropriate term", as const element = <input type="number" autocomplete="email" />; const target = element.attribute("autocomplete").get()!; - const document = Document.of([element]); + const document = h.document([element]); t.deepEqual(await evaluate(R10, { document }), [ failed(R10, target, { @@ -78,7 +77,7 @@ test("evaluate() fails an autocomplete attribute with a comma-separated list of const element = <input autocomplete="work,email" />; const target = element.attribute("autocomplete").get()!; - const document = Document.of([element]); + const document = h.document([element]); t.deepEqual(await evaluate(R10, { document }), [ failed(R10, target, { @@ -90,7 +89,7 @@ test("evaluate() fails an autocomplete attribute with a comma-separated list of test("evaluates() is inapplicable when there is no autocomplete attribute", async (t) => { const element = <input />; - const document = Document.of([element]); + const document = h.document([element]); t.deepEqual(await evaluate(R10, { document }), [inapplicable(R10)]); }); @@ -98,7 +97,7 @@ test("evaluates() is inapplicable when there is no autocomplete attribute", asyn test("evaluates() is inapplicable on empty autocomplete attribute", async (t) => { const element = <input autocomplete=" " />; - const document = Document.of([element]); + const document = h.document([element]); t.deepEqual(await evaluate(R10, { document }), [inapplicable(R10)]); }); @@ -106,7 +105,7 @@ test("evaluates() is inapplicable on empty autocomplete attribute", async (t) => test("evaluates() is inapplicable on input type who don't support autocomplete", async (t) => { const element = <input type="button" autocomplete="username" />; - const document = Document.of([element]); + const document = h.document([element]); t.deepEqual(await evaluate(R10, { document }), [inapplicable(R10)]); }); @@ -114,7 +113,7 @@ test("evaluates() is inapplicable on input type who don't support autocomplete", test("evaluates() is inapplicable on invisible elements", async (t) => { const element = <input style={{ display: "none" }} autocomplete="email" />; - const document = Document.of([element]); + const document = h.document([element]); t.deepEqual(await evaluate(R10, { document }), [inapplicable(R10)]); }); @@ -122,7 +121,7 @@ test("evaluates() is inapplicable on invisible elements", async (t) => { test("evaluates() is inapplicable on aria-disabled elements", async (t) => { const element = <input aria-disabled="true" autocomplete="email" />; - const document = Document.of([element]); + const document = h.document([element]); t.deepEqual(await evaluate(R10, { document }), [inapplicable(R10)]); }); @@ -130,7 +129,7 @@ test("evaluates() is inapplicable on aria-disabled elements", async (t) => { test("evaluates() is inapplicable on disabled elements", async (t) => { const element = <input disabled autocomplete="email" />; - const document = Document.of([element]); + const document = h.document([element]); t.deepEqual(await evaluate(R10, { document }), [inapplicable(R10)]); }); diff --git a/packages/alfa-rules/test/sia-r11/rule.spec.tsx b/packages/alfa-rules/test/sia-r11/rule.spec.tsx new file mode 100644 index 0000000000..099dc68f72 --- /dev/null +++ b/packages/alfa-rules/test/sia-r11/rule.spec.tsx @@ -0,0 +1,47 @@ +import { h } from "@siteimprove/alfa-dom"; +import { test } from "@siteimprove/alfa-test"; + +import R11, { Outcomes } from "../../src/sia-r11/rule"; + +import { evaluate } from "../common/evaluate"; +import { passed, failed, inapplicable } from "../common/outcome"; + +test(`evaluate() passes a link with a name given by content`, async (t) => { + const target = <a href="#">Foo</a>; + + const document = h.document([target]); + + t.deepEqual(await evaluate(R11, { document }), [ + passed(R11, target, { 1: Outcomes.HasName }), + ]); +}); + +test(`evaluate() fails a link with no name`, async (t) => { + const target = <a href="#"></a>; + + const document = h.document([target]); + + t.deepEqual(await evaluate(R11, { document }), [ + failed(R11, target, { 1: Outcomes.HasNoName }), + ]); +}); + +test(`evaluate() is inapplicable when there is no link`, async (t) => { + const document = h.document([<div></div>]); + + t.deepEqual(await evaluate(R11, { document }), [inapplicable(R11)]); +}); + +test(`evaluate() passes an image link with name given by the alt text`, async (t) => { + const target = ( + <a href="#"> + <img src="foo.jpg" alt="Foo" /> + </a> + ); + + const document = h.document([target]); + + t.deepEqual(await evaluate(R11, { document }), [ + passed(R11, target, { 1: Outcomes.HasName }), + ]); +}); diff --git a/packages/alfa-rules/test/sia-r12/rule.spec.tsx b/packages/alfa-rules/test/sia-r12/rule.spec.tsx new file mode 100644 index 0000000000..bf1e8609a3 --- /dev/null +++ b/packages/alfa-rules/test/sia-r12/rule.spec.tsx @@ -0,0 +1,93 @@ +import { h } from "@siteimprove/alfa-dom"; +import { test } from "@siteimprove/alfa-test"; + +import R12, { Outcomes } from "../../src/sia-r12/rule"; + +import { evaluate } from "../common/evaluate"; +import { passed, failed, inapplicable } from "../common/outcome"; + +test(`evaluates() passes a <button> with accessible name given by content`, async (t) => { + const target = <button>My button</button>; + + const document = h.document([target]); + + t.deepEqual(await evaluate(R12, { document }), [ + passed(R12, target, { + 1: Outcomes.HasName, + }), + ]); +}); + +test(`evaluates() passes a submit button element with an accessible name given + by the value attribute `, async (t) => { + const input = <input type="submit" value="Submit" />; + + const document = h.document([input]); + + t.deepEqual(await evaluate(R12, { document }), [ + passed(R12, input, { + 1: Outcomes.HasName, + }), + ]); +}); + +test(`evaluates() passes a <button> with a name given by the \`aria-label\` + attribute`, async (t) => { + const target = <button aria-label="My button"></button>; + + const document = h.document([target]); + + t.deepEqual(await evaluate(R12, { document }), [ + passed(R12, target, { + 1: Outcomes.HasName, + }), + ]); +}); + +test(`evaluates() fails a button with no accessible name`, async (t) => { + const target = <button></button>; + + const document = h.document([target]); + + t.deepEqual(await evaluate(R12, { document }), [ + failed(R12, target, { + 1: Outcomes.HasNoName, + }), + ]); +}); + +test(`evaluates() fails an element with \`button\` role without an accessible name`, async (t) => { + const target = <span role="button"></span>; + + const document = h.document([target]); + + t.deepEqual(await evaluate(R12, { document }), [ + failed(R12, target, { + 1: Outcomes.HasNoName, + }), + ]); +}); + +test(`evaluate() is inapplicable to image buttons`, async (t) => { + const target = <input type="image" value="download" alt="Download" />; + + const document = h.document([target]); + + t.deepEqual(await evaluate(R12, { document }), [inapplicable(R12)]); +}); + +test(`evaluate() is inapplicable to element with no button role`, async (t) => { + const target = <div>Press Here</div>; + + const document = h.document([target]); + + t.deepEqual(await evaluate(R12, { document }), [inapplicable(R12)]); +}); + +test(`evaluate() is inapplicable to button element with none role`, async (t) => { + const target = <button role="none" disabled></button>; + + const document = h.document([target]); + + t.deepEqual(await evaluate(R12, { document }), [inapplicable(R12)]); +}); diff --git a/packages/alfa-rules/test/sia-r13/rule.spec.tsx b/packages/alfa-rules/test/sia-r13/rule.spec.tsx index cc01cbfa54..11a59ce4d9 100644 --- a/packages/alfa-rules/test/sia-r13/rule.spec.tsx +++ b/packages/alfa-rules/test/sia-r13/rule.spec.tsx @@ -1,7 +1,6 @@ +import { h } from "@siteimprove/alfa-dom"; import { test } from "@siteimprove/alfa-test"; -import { Document } from "@siteimprove/alfa-dom"; - import R13, { Outcomes } from "../../src/sia-r13/rule"; import { evaluate } from "../common/evaluate"; @@ -11,7 +10,7 @@ test(`evaluates() passes an <iframe> element with an accessible name given by the title attribute`, async (t) => { const target = <iframe title="iframe" srcdoc="Hello World!" />; - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R13, { document }), [ passed(R13, target, { @@ -24,7 +23,7 @@ test(`evaluates() passes an <iframe> element with an accessible name given by the aria-label attribute`, async (t) => { const target = <iframe aria-label="iframe" srcdoc="Hello World!" />; - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R13, { document }), [ passed(R13, target, { @@ -37,7 +36,7 @@ test(`evaluates() passes an <iframe> element with an accessible name given by the aria-labelledby attribute`, async (t) => { const target = <iframe aria-labelledby="label" srcdoc="Hello World!" />; - const document = Document.of([<span id="label">iframe</span>, target]); + const document = h.document([<span id="label">iframe</span>, target]); t.deepEqual(await evaluate(R13, { document }), [ passed(R13, target, { @@ -49,7 +48,7 @@ test(`evaluates() passes an <iframe> element with an accessible name given by test("evaluates() fails an <iframe> element without an accessible name", async (t) => { const target = <iframe srcdoc="Hello World!" />; - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R13, { document }), [ failed(R13, target, { @@ -59,7 +58,7 @@ test("evaluates() fails an <iframe> element without an accessible name", async ( }); test("evaluate() is inapplicable to a document without <iframe> elements", async (t) => { - const document = Document.empty(); + const document = h.document([]); t.deepEqual(await evaluate(R13, { document }), [inapplicable(R13)]); }); diff --git a/packages/alfa-rules/test/sia-r14/rule.spec.tsx b/packages/alfa-rules/test/sia-r14/rule.spec.tsx index 3fc87ad380..3dd52d6fd2 100644 --- a/packages/alfa-rules/test/sia-r14/rule.spec.tsx +++ b/packages/alfa-rules/test/sia-r14/rule.spec.tsx @@ -1,7 +1,6 @@ +import { h } from "@siteimprove/alfa-dom"; import { test } from "@siteimprove/alfa-test"; -import { Document } from "@siteimprove/alfa-dom"; - import R14, { Outcomes } from "../../src/sia-r14/rule"; import { evaluate } from "../common/evaluate"; @@ -11,11 +10,11 @@ test(`evaluate() passes a <button> element whose perceivable text content matches its accessible name set by aria-label`, async (t) => { const target = <button aria-label="Hello world">Hello world</button>; - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R14, { document }), [ passed(R14, target, { - 1: Outcomes.VisibleIsInName, + 1: Outcomes.VisibleIsInName("hello world", "hello world"), }), ]); }); @@ -24,11 +23,11 @@ test(`evaluate() passes a <button> element whose perceivable text content is included in its accessible name set by aria-label`, async (t) => { const target = <button aria-label="Hello world">Hello</button>; - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R14, { document }), [ passed(R14, target, { - 1: Outcomes.VisibleIsInName, + 1: Outcomes.VisibleIsInName("hello", "hello world"), }), ]); }); @@ -37,11 +36,11 @@ test(`evaluate() fails a <button> element whose perceivable text content is not included in its accessible name set by aria-label`, async (t) => { const target = <button aria-label="Hello">Hello world</button>; - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R14, { document }), [ failed(R14, target, { - 1: Outcomes.VisibleIsNotInName, + 1: Outcomes.VisibleIsNotInName("hello world", "hello"), }), ]); }); @@ -53,11 +52,11 @@ test(`evaluate() ignores non-perceivable text content`, async (t) => { </button> ); - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R14, { document }), [ passed(R14, target, { - 1: Outcomes.VisibleIsInName, + 1: Outcomes.VisibleIsInName("hello", "hello"), }), ]); }); diff --git a/packages/alfa-rules/test/sia-r15/rule.spec.tsx b/packages/alfa-rules/test/sia-r15/rule.spec.tsx index 873212a6fd..ce0a29f277 100644 --- a/packages/alfa-rules/test/sia-r15/rule.spec.tsx +++ b/packages/alfa-rules/test/sia-r15/rule.spec.tsx @@ -1,7 +1,6 @@ -import { h } from "@siteimprove/alfa-dom/h"; +import { h } from "@siteimprove/alfa-dom"; import { test } from "@siteimprove/alfa-test"; -import { Document } from "@siteimprove/alfa-dom"; import { Response } from "@siteimprove/alfa-http"; import { URL } from "@siteimprove/alfa-url"; @@ -19,7 +18,7 @@ test("evaluate() passes when two iframes embed the exact same resource", async ( <iframe aria-label="Foo" src="https://somewhere.com/foo.html" />, ]; - const document = Document.of(target); + const document = h.document(target); t.deepEqual(await evaluate(R15, { document }), [ passed(R15, Group.of(target), { @@ -34,7 +33,7 @@ test("evaluate() passes when two iframes embed the exact same resource via srcdo <iframe aria-label="Foo" srcdoc="<span>foo</span>" />, ]; - const document = Document.of(target); + const document = h.document(target); t.deepEqual(await evaluate(R15, { document }), [ passed(R15, Group.of(target), { @@ -49,7 +48,7 @@ test("evaluate() passes when two iframes embed equivalent resources", async (t) <iframe aria-label="Foo" src="https://somewhere.com/foo2.html" />, ]; - const document = Document.of(target); + const document = h.document(target); t.deepEqual( await evaluate( @@ -71,7 +70,7 @@ test("evaluate() passes when toplevel and nested iframe embed the same resource" <iframe aria-label="Foo" src="https://somewhere.com/foo.html" />, ]; - const document = Document.of([ + const document = h.document([ target[0], <iframe title="Container">{h.document([target[1]])}</iframe>, ]); @@ -89,7 +88,7 @@ test("evaluate() fails when two iframes embed different resources", async (t) => <iframe aria-label="Foobar" src="https://somewhere.com/bar.html" />, ]; - const document = Document.of(target); + const document = h.document(target); t.deepEqual( await evaluate( @@ -106,7 +105,7 @@ test("evaluate() fails when two iframes embed different resources", async (t) => }); test("evaluate() is inapplicable when there is no two iframe with the same name", async (t) => { - const document = Document.of([ + const document = h.document([ <iframe title="Foo" src="https://somewhere.com/foo.html" />, <iframe aria-label="Bar" src="https://somewhere.com/bar.html" />, ]); @@ -120,7 +119,7 @@ test("evaluate() can't tell if URLs are identical but invalid", async (t) => { <iframe aria-label="Foo" src="https:////////@@@" />, ]; - const document = Document.of(target); + const document = h.document(target); t.deepEqual(await evaluate(R15, { document }), [ cantTell(R15, Group.of(target)), @@ -130,7 +129,7 @@ test("evaluate() can't tell if URLs are identical but invalid", async (t) => { test("evaluate() can't tell if there is no source", async (t) => { const target = [<iframe title="Foo" />, <iframe aria-label="Foo" />]; - const document = Document.of(target); + const document = h.document(target); t.deepEqual(await evaluate(R15, { document }), [ cantTell(R15, Group.of(target)), @@ -143,7 +142,7 @@ test("evaluate() passes when two iframes embed the same resource up to trailing <iframe aria-label="Foo" src="https://somewhere.com" />, ]; - const document = Document.of(target); + const document = h.document(target); t.deepEqual(await evaluate(R15, { document }), [ passed(R15, Group.of(target), { @@ -162,7 +161,7 @@ test("evaluate() correctly resolves relative URLs", async (t) => { <iframe title="Foo" src="../to/foo.html" />, ]; - const document = Document.of(target); + const document = h.document(target); t.deepEqual( await evaluate(R15, { diff --git a/packages/alfa-rules/test/sia-r16/rule.spec.tsx b/packages/alfa-rules/test/sia-r16/rule.spec.tsx index a7af36b0a4..dc0d7af429 100644 --- a/packages/alfa-rules/test/sia-r16/rule.spec.tsx +++ b/packages/alfa-rules/test/sia-r16/rule.spec.tsx @@ -1,8 +1,10 @@ +import { h } from "@siteimprove/alfa-dom"; import { test } from "@siteimprove/alfa-test"; -import { Document } from "@siteimprove/alfa-dom"; - -import R16, { Outcomes } from "../../src/sia-r16/rule"; +import R16, { + Outcomes, + RoleAndRequiredAttributes, +} from "../../src/sia-r16/rule"; import { evaluate } from "../common/evaluate"; import { passed, failed, inapplicable } from "../common/outcome"; @@ -11,11 +13,13 @@ test(`evaluate() passes a <div> element with a role of checkbox and an aria-checked attribute`, async (t) => { const target = <div role="checkbox" aria-checked="true" />; - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R16, { document }), [ passed(R16, target, { - 1: Outcomes.HasAllStates, + 1: Outcomes.HasAllStates( + RoleAndRequiredAttributes.of("", "checkbox", ["aria-checked"], []) + ), }), ]); }); @@ -23,11 +27,13 @@ test(`evaluate() passes a <div> element with a role of checkbox and an test(`evaluate() passes an <input> element with a type of checkbox`, async (t) => { const target = <input type="checkbox" />; - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R16, { document }), [ passed(R16, target, { - 1: Outcomes.HasAllStates, + 1: Outcomes.HasAllStates( + RoleAndRequiredAttributes.of("", "checkbox", ["aria-checked"], []) + ), }), ]); }); @@ -35,11 +41,13 @@ test(`evaluate() passes an <input> element with a type of checkbox`, async (t) = test(`evaluate() passes an <hr> element`, async (t) => { const target = <hr />; - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R16, { document }), [ passed(R16, target, { - 1: Outcomes.HasAllStates, + 1: Outcomes.HasAllStates( + RoleAndRequiredAttributes.of("", "separator", [], []) + ), }), ]); }); @@ -47,11 +55,13 @@ test(`evaluate() passes an <hr> element`, async (t) => { test(`evaluate() passes a non-focusable <div> element with a role of separator`, async (t) => { const target = <div role="separator" />; - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R16, { document }), [ passed(R16, target, { - 1: Outcomes.HasAllStates, + 1: Outcomes.HasAllStates( + RoleAndRequiredAttributes.of("", "separator", [], []) + ), }), ]); }); @@ -60,11 +70,13 @@ test(`evaluate() passes a focusable <div> element with a role of separator and an aria-valuenow attribute`, async (t) => { const target = <div role="separator" tabindex="0" aria-valuenow="50" />; - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R16, { document }), [ passed(R16, target, { - 1: Outcomes.HasAllStates, + 1: Outcomes.HasAllStates( + RoleAndRequiredAttributes.of("", "separator", ["aria-valuenow"], []) + ), }), ]); }); @@ -73,11 +85,18 @@ test(`evaluate() fails a <div> element with a role of checkbox and no aria-checked attribute`, async (t) => { const target = <div role="checkbox" />; - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R16, { document }), [ failed(R16, target, { - 1: Outcomes.HasNotAllStates, + 1: Outcomes.HasNotAllStates( + RoleAndRequiredAttributes.of( + "", + "checkbox", + ["aria-checked"], + ["aria-checked"] + ) + ), }), ]); }); @@ -86,11 +105,18 @@ test(`evaluate() fails a focusable <div> element with a role of separator and no aria-valuenow attribute`, async (t) => { const target = <div role="separator" tabindex="0" />; - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R16, { document }), [ failed(R16, target, { - 1: Outcomes.HasNotAllStates, + 1: Outcomes.HasNotAllStates( + RoleAndRequiredAttributes.of( + "", + "separator", + ["aria-valuenow"], + ["aria-valuenow"] + ) + ), }), ]); }); @@ -98,7 +124,7 @@ test(`evaluate() fails a focusable <div> element with a role of separator and no test("evaluate() is inapplicable to elements that are not exposed", async (t) => { const target = <div role="combobox" style={{ display: "none" }} />; - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R16, { document }), [inapplicable(R16)]); }); diff --git a/packages/alfa-rules/test/sia-r17/rule.spec.tsx b/packages/alfa-rules/test/sia-r17/rule.spec.tsx new file mode 100644 index 0000000000..e1828268c9 --- /dev/null +++ b/packages/alfa-rules/test/sia-r17/rule.spec.tsx @@ -0,0 +1,145 @@ +import { h } from "@siteimprove/alfa-dom"; +import { test } from "@siteimprove/alfa-test"; + +import R17, { Outcomes } from "../../src/sia-r17/rule"; + +import { evaluate } from "../common/evaluate"; +import { passed, failed, inapplicable } from "../common/outcome"; + +test(`evaluate() passes an element which is not focusable by default`, async (t) => { + const target = <p aria-hidden="true">Some text</p>; + + const document = h.document([target]); + + t.deepEqual(await evaluate(R17, { document }), [ + passed(R17, target, { + 1: Outcomes.IsNotTabbable, + }), + ]); +}); + +test(`evaluate() passes an element which content is hidden`, async (t) => { + const target = ( + <div aria-hidden="true"> + <a href="/" style={{ display: "none" }}> + Link + </a> + </div> + ); + + const document = h.document([target]); + + t.deepEqual(await evaluate(R17, { document }), [ + passed(R17, target, { + 1: Outcomes.IsNotTabbable, + }), + ]); +}); + +test(`evaluate() passes an element whose content is taken out of sequential + focus order using tabindex`, async (t) => { + const target = ( + <div aria-hidden="true"> + <button tabindex="-1">Some button</button> + </div> + ); + + const document = h.document([target]); + + t.deepEqual(await evaluate(R17, { document }), [ + passed(R17, target, { + 1: Outcomes.IsNotTabbable, + }), + ]); +}); + +test(`evaluate() passes an element which is disabled`, async (t) => { + const target = <input disabled aria-hidden="true" />; + + const document = h.document([target]); + + t.deepEqual(await evaluate(R17, { document }), [ + passed(R17, target, { + 1: Outcomes.IsNotTabbable, + }), + ]); +}); + +test(`evaluate() fails an element with focusable content`, async (t) => { + const target = ( + <div aria-hidden="true"> + <a href="/">Link</a> + </div> + ); + + const document = h.document([target]); + + t.deepEqual(await evaluate(R17, { document }), [ + failed(R17, target, { + 1: Outcomes.IsTabbable, + }), + ]); +}); + +test(`evaluate() fails an element with an \`aria-hidden\` ancestor`, async (t) => { + const target = ( + <div aria-hidden="true"> + <div aria-hidden="false"> + <button>Some button</button> + </div> + </div> + ); + + const document = h.document([target]); + + t.deepEqual(await evaluate(R17, { document }), [ + failed(R17, target, { + 1: Outcomes.IsTabbable, + }), + ]); +}); + +test(`evaluate() fails an element with focusable content through tabindex`, async (t) => { + const target = ( + <p tabindex="0" aria-hidden="true"> + Some text + </p> + ); + + const document = h.document([target]); + + t.deepEqual(await evaluate(R17, { document }), [ + failed(R17, target, { + 1: Outcomes.IsTabbable, + }), + ]); +}); + +test(`evaluate() fails a focusable summary element`, async (t) => { + const target = ( + <details aria-hidden="true"> + <summary>Some button</summary> + <p>Some details</p> + </details> + ); + + const document = h.document([target]); + + t.deepEqual(await evaluate(R17, { document }), [ + failed(R17, target, { + 1: Outcomes.IsTabbable, + }), + ]); +}); + +test(`evaluate() is inapplicable when aria-hidden has incorrect value`, async (t) => { + const target = ( + <div aria-hidden="yes"> + <p>Some text</p> + </div> + ); + + const document = h.document([target]); + + t.deepEqual(await evaluate(R17, { document }), [inapplicable(R17)]); +}); diff --git a/packages/alfa-rules/test/sia-r18/rule.spec.tsx b/packages/alfa-rules/test/sia-r18/rule.spec.tsx new file mode 100644 index 0000000000..fac9820e68 --- /dev/null +++ b/packages/alfa-rules/test/sia-r18/rule.spec.tsx @@ -0,0 +1,174 @@ +import { h } from "@siteimprove/alfa-dom"; +import { test } from "@siteimprove/alfa-test"; + +import R18, { Outcomes } from "../../src/sia-r18/rule"; + +import { evaluate } from "../common/evaluate"; +import { passed, failed, inapplicable } from "../common/outcome"; + +test(`evaluate() passes a button with aria-pressed state`, async (t) => { + const target = <button aria-pressed="false">My button</button>; + + const document = h.document([target]); + + t.deepEqual(await evaluate(R18, { document }), [ + passed(R18, target.attribute("aria-pressed").get(), { + 1: Outcomes.IsAllowed, + }), + ]); +}); + +test(`evaluate() passes a div element with button role, and an aria-pressed state`, async (t) => { + const target = ( + <div role="button" aria-pressed="false"> + My button + </div> + ); + + const document = h.document([target]); + + t.deepEqual(await evaluate(R18, { document }), [ + passed(R18, target.attribute("aria-pressed").get(), { + 1: Outcomes.IsAllowed, + }), + ]); +}); + +test(`evaluate() passes a div element with aria busy state`, async (t) => { + const target = <div aria-busy="true">My busy div</div>; + + const document = h.document([target]); + + t.deepEqual(await evaluate(R18, { document }), [ + passed(R18, target.attribute("aria-busy").get(), { + 1: Outcomes.IsAllowed, + }), + ]); +}); + +test(`evaluate() passes a div element with button role, and an aria-label attribute`, async (t) => { + const target = ( + <div role="button" aria-label="OK"> + ✓ + </div> + ); + + const document = h.document([target]); + + t.deepEqual(await evaluate(R18, { document }), [ + passed(R18, target.attribute("aria-label").get(), { + 1: Outcomes.IsAllowed, + }), + ]); +}); + +test(`evaluate() passes a div element with checkbox role, and an aria-checked state`, async (t) => { + const target = ( + <div role="checkbox" aria-checked="false"> + My checkbox + </div> + ); + + const document = h.document([target]); + + t.deepEqual(await evaluate(R18, { document }), [ + passed(R18, target.attribute("aria-checked").get(), { + 1: Outcomes.IsAllowed, + }), + ]); +}); + +test(`evaluate() passes a div element with checkbox role, and an aria-controls state`, async (t) => { + const target = ( + <div role="combobox" aria-controls="id1" aria-expanded="false"> + My combobox + </div> + ); + + const document = h.document([target]); + + t.deepEqual(await evaluate(R18, { document }), [ + passed(R18, target.attribute("aria-controls").get(), { + 1: Outcomes.IsAllowed, + }), + passed(R18, target.attribute("aria-expanded").get(), { + 1: Outcomes.IsAllowed, + }), + ]); +}); + +test(`evaluate() passes a div element with checkbox role, and both + aria-controls and aria-expanded states`, async (t) => { + const target = ( + <div role="combobox" aria-controls="id1" aria-expanded="false"> + My combobox + </div> + ); + + const document = h.document([target]); + + t.deepEqual(await evaluate(R18, { document }), [ + passed(R18, target.attribute("aria-controls").get(), { + 1: Outcomes.IsAllowed, + }), + passed(R18, target.attribute("aria-expanded").get(), { + 1: Outcomes.IsAllowed, + }), + ]); +}); + +test(`evaluate() passes a div element with checkbox role, with both + aria-expanded and empty aria-controls state`, async (t) => { + const target = ( + <div role="combobox" aria-expanded="false" aria-controls=""> + My combobox + </div> + ); + + const document = h.document([target]); + + t.deepEqual(await evaluate(R18, { document }), [ + passed(R18, target.attribute("aria-expanded").get(), { + 1: Outcomes.IsAllowed, + }), + passed(R18, target.attribute("aria-controls").get(), { + 1: Outcomes.IsAllowed, + }), + ]); +}); + +test(`evaluate() passes a button element with none role and aria-pressed`, async (t) => { + const target = ( + <button role="none" aria-pressed="false"> + ACT rules are cool! + </button> + ); + + const document = h.document([target]); + + t.deepEqual(await evaluate(R18, { document }), [ + passed(R18, target.attribute("aria-pressed").get(), { + 1: Outcomes.IsAllowed, + }), + ]); +}); + +test(`evaluate() fails a button with aria-sort state, and no property`, async (t) => { + const target = <button aria-sort="">Sort by year</button>; + + const document = h.document([target]); + + t.deepEqual(await evaluate(R18, { document }), [ + failed(R18, target.attribute("aria-sort").get(), { + 1: Outcomes.IsNotAllowed, + }), + ]); +}); + +test(`evaluate() is inapplicable for a div element with no aria attribute`, async (t) => { + const target = <div role="region">A region of content</div>; + + const document = h.document([target]); + + t.deepEqual(await evaluate(R18, { document }), [inapplicable(R18)]); +}); diff --git a/packages/alfa-rules/test/sia-r19/rule.spec.tsx b/packages/alfa-rules/test/sia-r19/rule.spec.tsx new file mode 100644 index 0000000000..e2341dd3a7 --- /dev/null +++ b/packages/alfa-rules/test/sia-r19/rule.spec.tsx @@ -0,0 +1,246 @@ +import { h } from "@siteimprove/alfa-dom"; +import { test } from "@siteimprove/alfa-test"; + +import R19, { Outcomes } from "../../src/sia-r19/rule"; + +import { evaluate } from "../common/evaluate"; +import { passed, failed, inapplicable } from "../common/outcome"; + +test(`evaluate() passes an aria-required attribute with a valid true value`, async (t) => { + const target = <div role="textbox" aria-required="true"></div>; + + const document = h.document([target]); + + t.deepEqual(await evaluate(R19, { document }), [ + passed(R19, target.attribute("aria-required").get(), { + 1: Outcomes.HasValidValue, + }), + ]); +}); + +test("evaluate() passes an aria-expanded attribute with a valid undefined value", async (t) => { + const target = ( + <div role="button" aria-expanded="undefined"> + A button + </div> + ); + + const document = h.document([target]); + + t.deepEqual(await evaluate(R19, { document }), [ + passed(R19, target.attribute("aria-expanded").get(), { + 1: Outcomes.HasValidValue, + }), + ]); +}); + +test("evaluate() passes an aria-pressed attribute with a valid tristate value", async (t) => { + const target = ( + <div role="button" aria-pressed="mixed"> + An other button + </div> + ); + + const document = h.document([target]); + + t.deepEqual(await evaluate(R19, { document }), [ + passed(R19, target.attribute("aria-pressed").get(), { + 1: Outcomes.HasValidValue, + }), + ]); +}); + +test("evaluate() passes an aria-errormessage attribute with a valid ID reference value", async (t) => { + const target = <div role="textbox" aria-errormessage="my-error"></div>; + + const document = h.document([target]); + + t.deepEqual(await evaluate(R19, { document }), [ + passed(R19, target.attribute("aria-errormessage").get(), { + 1: Outcomes.HasValidValue, + }), + ]); +}); + +test("evaluate() passes an aria-rowindex attribute with a valid integer value", async (t) => { + const target = ( + <div role="gridcell" aria-rowindex="2"> + Fred + </div> + ); + + const document = h.document([target]); + + t.deepEqual(await evaluate(R19, { document }), [ + passed(R19, target.attribute("aria-rowindex").get(), { + 1: Outcomes.HasValidValue, + }), + ]); +}); + +test(`evaluate() passes aria-valuemin, aria-valuemax and aria-valuenow + attributes with valid number values`, async (t) => { + const target = ( + <div + role="spinbutton" + aria-valuemin="1.0" + aria-valuemax="2.0" + aria-valuenow="1.5" + ></div> + ); + + const document = h.document([target]); + + t.deepEqual(await evaluate(R19, { document }), [ + passed(R19, target.attribute("aria-valuemin").get(), { + 1: Outcomes.HasValidValue, + }), + passed(R19, target.attribute("aria-valuemax").get(), { + 1: Outcomes.HasValidValue, + }), + passed(R19, target.attribute("aria-valuenow").get(), { + 1: Outcomes.HasValidValue, + }), + ]); +}); + +test("evaluate() passes an aria-placeholder attribute with a valid string value", async (t) => { + const target = ( + <div role="textbox" aria-placeholder="MM-DD-YYYY"> + MM-DD-YYYY + </div> + ); + + const document = h.document([target]); + + t.deepEqual(await evaluate(R19, { document }), [ + passed(R19, target.attribute("aria-placeholder").get(), { + 1: Outcomes.HasValidValue, + }), + ]); +}); + +test("evaluate() passes an aria-dropeffect property with a valid token list value", async (t) => { + const target = <div role="dialog" aria-dropeffect="copy move"></div>; + + const document = h.document([target]); + + t.deepEqual(await evaluate(R19, { document }), [ + passed(R19, target.attribute("aria-dropeffect").get(), { + 1: Outcomes.HasValidValue, + }), + ]); +}); + +test("evaluate() fails an aria-expanded state with an invalid true/false/undefined value", async (t) => { + const target = ( + <div role="button" aria-expanded="mixed"> + A button + </div> + ); + + const document = h.document([target]); + + t.deepEqual(await evaluate(R19, { document }), [ + failed(R19, target.attribute("aria-expanded").get(), { + 1: Outcomes.HasNoValidValue, + }), + ]); +}); + +test("evaluate() fails an aria-pressed state with an invalid tristate value", async (t) => { + const target = ( + <div role="button" aria-pressed="horizontal"> + An other button + </div> + ); + + const document = h.document([target]); + + t.deepEqual(await evaluate(R19, { document }), [ + failed(R19, target.attribute("aria-pressed").get(), { + 1: Outcomes.HasNoValidValue, + }), + ]); +}); + +test("evaluate() fails an aria-rowindex property with an invalid integer value", async (t) => { + const target = ( + <div role="gridcell" aria-rowindex="2.5"> + Fred + </div> + ); + + const document = h.document([target]); + + t.deepEqual(await evaluate(R19, { document }), [ + failed(R19, target.attribute("aria-rowindex").get(), { + 1: Outcomes.HasNoValidValue, + }), + ]); +}); + +test("evaluate() fails an aria-live property with an invalid token value", async (t) => { + const target = <div role="main" aria-live="nope"></div>; + + const document = h.document([target]); + + t.deepEqual(await evaluate(R19, { document }), [ + failed(R19, target.attribute("aria-live").get(), { + 1: Outcomes.HasNoValidValue, + }), + ]); +}); + +test("evaluate() fails an aria-rowindex property with an invalid integer value", async (t) => { + const target = ( + <div + role="spinbutton" + aria-valuemin="one" + aria-valuemax="three" + aria-valuenow="two" + ></div> + ); + + const document = h.document([target]); + + t.deepEqual(await evaluate(R19, { document }), [ + failed(R19, target.attribute("aria-valuemin").get(), { + 1: Outcomes.HasNoValidValue, + }), + failed(R19, target.attribute("aria-valuemax").get(), { + 1: Outcomes.HasNoValidValue, + }), + failed(R19, target.attribute("aria-valuenow").get(), { + 1: Outcomes.HasNoValidValue, + }), + ]); +}); + +test("evaluate() fails an aria-errormessage property with an invalid ID reference value", async (t) => { + const target = <div role="textbox" aria-errormessage="error1 error2"></div>; + + const document = h.document([target]); + + t.deepEqual(await evaluate(R19, { document }), [ + failed(R19, target.attribute("aria-errormessage").get(), { + 1: Outcomes.HasNoValidValue, + }), + ]); +}); + +test("evaluate() is inapplicable when an element does not have any ARIA attribute", async (t) => { + const document = h.document([<div>Some Content</div>]); + + t.deepEqual(await evaluate(R19, { document }), [inapplicable(R19)]); +}); + +test("evaluate() is inapplicable when aria-checked state has an empty value", async (t) => { + const document = h.document([ + <div role="checkbox" aria-checked> + Accept terms and conditions + </div>, + ]); + + t.deepEqual(await evaluate(R19, { document }), [inapplicable(R19)]); +}); diff --git a/packages/alfa-rules/test/sia-r2/rule.spec.tsx b/packages/alfa-rules/test/sia-r2/rule.spec.tsx index b3c42c5303..48bcb54106 100644 --- a/packages/alfa-rules/test/sia-r2/rule.spec.tsx +++ b/packages/alfa-rules/test/sia-r2/rule.spec.tsx @@ -1,7 +1,6 @@ +import { h } from "@siteimprove/alfa-dom"; import { test } from "@siteimprove/alfa-test"; -import { Document } from "@siteimprove/alfa-dom"; - import R2, { Outcomes } from "../../src/sia-r2/rule"; import { evaluate } from "../common/evaluate"; @@ -10,7 +9,7 @@ import { passed, failed, inapplicable } from "../common/outcome"; test("evaluate() passes an <img> element with an accessible name", async (t) => { const target = <img alt="Hello world" />; - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R2, { document }), [ passed(R2, target, { @@ -22,7 +21,7 @@ test("evaluate() passes an <img> element with an accessible name", async (t) => test("evaluate() fails an <img> element without an accessible name", async (t) => { const target = <img />; - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R2, { document }), [ failed(R2, target, { @@ -32,19 +31,19 @@ test("evaluate() fails an <img> element without an accessible name", async (t) = }); test("evaluate() is inapplicable to an <img> element with a presentational role", async (t) => { - const document = Document.of([<img role="none" />]); + const document = h.document([<img role="none" />]); t.deepEqual(await evaluate(R2, { document }), [inapplicable(R2)]); }); test("evaluate() is inapplicable to an <img> element that is hidden", async (t) => { - const document = Document.of([<img hidden alt="Hello world" />]); + const document = h.document([<img hidden alt="Hello world" />]); t.deepEqual(await evaluate(R2, { document }), [inapplicable(R2)]); }); test("evaluate() is inapplicable to a document without images", async (t) => { - const document = Document.empty(); + const document = h.document([]); t.deepEqual(await evaluate(R2, { document }), [inapplicable(R2)]); }); @@ -52,7 +51,7 @@ test("evaluate() is inapplicable to a document without images", async (t) => { // https://github.com/siteimprove/alfa/issues/444 test(`evaluate() is inapplicable to a non-draggable <img> element with a presentational role`, async (t) => { - const document = Document.of([<img role="none" draggable="false" />]); + const document = h.document([<img role="none" draggable="false" />]); t.deepEqual(await evaluate(R2, { document }), [inapplicable(R2)]); }); diff --git a/packages/alfa-rules/test/sia-r20/rule.spec.tsx b/packages/alfa-rules/test/sia-r20/rule.spec.tsx new file mode 100644 index 0000000000..3a2bf18ef4 --- /dev/null +++ b/packages/alfa-rules/test/sia-r20/rule.spec.tsx @@ -0,0 +1,80 @@ +import { h } from "@siteimprove/alfa-dom"; +import { test } from "@siteimprove/alfa-test"; + +import R20, { Outcomes } from "../../src/sia-r20/rule"; + +import { evaluate } from "../common/evaluate"; +import { passed, failed, inapplicable } from "../common/outcome"; + +test(`evaluate() passes an article with a valid aria-atomic attribute`, async (t) => { + const target = ( + <article aria-atomic="true"> + This is a description of something cool... + </article> + ); + + const document = h.document([target]); + + t.deepEqual(await evaluate(R20, { document }), [ + passed(R20, target.attribute("aria-atomic").get(), { + 1: Outcomes.IsDefined, + }), + ]); +}); + +test(`evaluate() passes a div element with a valid aria-modal attribute`, async (t) => { + const target = <div aria-modal="true">Contains modal content...</div>; + + const document = h.document([target]); + + t.deepEqual(await evaluate(R20, { document }), [ + passed(R20, target.attribute("aria-modal").get(), { + 1: Outcomes.IsDefined, + }), + ]); +}); + +test(`evaluate() passes a div element with different valid aria-* attributes`, async (t) => { + const target = ( + <div + contenteditable="true" + aria-multiline="true" + aria-label="Enter your hobbies" + aria-required="true" + ></div> + ); + + const document = h.document([target]); + + t.deepEqual(await evaluate(R20, { document }), [ + passed(R20, target.attribute("aria-multiline").get(), { + 1: Outcomes.IsDefined, + }), + passed(R20, target.attribute("aria-label").get(), { + 1: Outcomes.IsDefined, + }), + passed(R20, target.attribute("aria-required").get(), { + 1: Outcomes.IsDefined, + }), + ]); +}); + +test(`evaluate() fails a div element which has a non official aria attribute`, async (t) => { + const target = <div aria-not-checked="true">List Item</div>; + + const document = h.document([target]); + + t.deepEqual(await evaluate(R20, { document }), [ + failed(R20, target.attribute("aria-not-checked").get(), { + 1: Outcomes.IsNotDefined, + }), + ]); +}); + +test(`evaluate() is not applicable to a canvas element with no aria-* attribute`, async (t) => { + const target = <canvas> </canvas>; + + const document = h.document([target]); + + t.deepEqual(await evaluate(R20, { document }), [inapplicable(R20)]); +}); diff --git a/packages/alfa-rules/test/sia-r21/rule.spec.tsx b/packages/alfa-rules/test/sia-r21/rule.spec.tsx index f4c1e34e8b..00b6d03ed8 100644 --- a/packages/alfa-rules/test/sia-r21/rule.spec.tsx +++ b/packages/alfa-rules/test/sia-r21/rule.spec.tsx @@ -1,8 +1,6 @@ -import { h } from "@siteimprove/alfa-dom/h"; +import { h } from "@siteimprove/alfa-dom"; import { test } from "@siteimprove/alfa-test"; -import { Document } from "@siteimprove/alfa-dom"; - import R21, { Outcomes } from "../../src/sia-r21/rule"; import { evaluate } from "../common/evaluate"; @@ -11,7 +9,7 @@ import { passed, failed, inapplicable } from "../common/outcome"; test("evaluates() passes an element with a single valid role", async (t) => { const target = h.attribute("role", "button"); - const document = Document.of([h("button", [target])]); + const document = h.document([h("button", [target])]); t.deepEqual(await evaluate(R21, { document }), [ passed(R21, target, { @@ -23,7 +21,7 @@ test("evaluates() passes an element with a single valid role", async (t) => { test("evaluates() passes an element with multiple valid roles", async (t) => { const target = h.attribute("role", "button link"); - const document = Document.of([h("button", [target])]); + const document = h.document([h("button", [target])]); t.deepEqual(await evaluate(R21, { document }), [ passed(R21, target, { @@ -35,7 +33,7 @@ test("evaluates() passes an element with multiple valid roles", async (t) => { test("evaluates() fails an element with an invalid role", async (t) => { const target = h.attribute("role", "btn"); - const document = Document.of([h("button", [target])]); + const document = h.document([h("button", [target])]); t.deepEqual(await evaluate(R21, { document }), [ failed(R21, target, { @@ -47,7 +45,7 @@ test("evaluates() fails an element with an invalid role", async (t) => { test("evaluates() fails an element with both a valid and an invalid role", async (t) => { const target = h.attribute("role", "btn link"); - const document = Document.of([h("button", [target])]); + const document = h.document([h("button", [target])]); t.deepEqual(await evaluate(R21, { document }), [ failed(R21, target, { @@ -57,13 +55,13 @@ test("evaluates() fails an element with both a valid and an invalid role", async }); test("evaluate() is inapplicable when there is no role attribute", async (t) => { - const document = Document.of([<button />]); + const document = h.document([<button />]); t.deepEqual(await evaluate(R21, { document }), [inapplicable(R21)]); }); test("evaluate() is inapplicable when a role attribute is only whitespace", async (t) => { - const document = Document.of([<button role=" " />]); + const document = h.document([<button role=" " />]); t.deepEqual(await evaluate(R21, { document }), [inapplicable(R21)]); }); diff --git a/packages/alfa-rules/test/sia-r24/rule.spec.tsx b/packages/alfa-rules/test/sia-r24/rule.spec.tsx index 180171fd07..2873dd4eba 100644 --- a/packages/alfa-rules/test/sia-r24/rule.spec.tsx +++ b/packages/alfa-rules/test/sia-r24/rule.spec.tsx @@ -1,6 +1,6 @@ +import { h } from "@siteimprove/alfa-dom"; import { test } from "@siteimprove/alfa-test"; -import { Document } from "@siteimprove/alfa-dom"; import { Option, None } from "@siteimprove/alfa-option"; import R24, { Outcomes } from "../../src/sia-r24/rule"; @@ -20,7 +20,7 @@ test(`evaluate() passes when non-streaming video elements have all audio and const transcript = <span id="transcript">Transcript</span>; - const document = Document.of([target, transcript]); + const document = h.document([target, transcript]); t.deepEqual( await evaluate( @@ -49,7 +49,7 @@ test(`evaluate() fails when non-streaming video elements have no audio and </video> ); - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual( await evaluate( @@ -78,7 +78,7 @@ test("evaluate() can't tell when some questions are left unanswered", async (t) </video> ); - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual( await evaluate( @@ -95,13 +95,13 @@ test("evaluate() can't tell when some questions are left unanswered", async (t) }); test("evaluate() is inapplicable to a document without <video> elements", async (t) => { - const document = Document.of([<img src="foo.mp4" />]); + const document = h.document([<img src="foo.mp4" />]); t.deepEqual(await evaluate(R24, { document }), [inapplicable(R24)]); }); test("evaluate() is inapplicable when applicability questions are unanswered", async (t) => { - const document = Document.of([ + const document = h.document([ <video controls> <source src="foo.mp4" type="video/mp4" /> <source src="foo.webm" type="video/webm" /> diff --git a/packages/alfa-rules/test/sia-r3/rule.spec.tsx b/packages/alfa-rules/test/sia-r3/rule.spec.tsx new file mode 100644 index 0000000000..25cd46cb0b --- /dev/null +++ b/packages/alfa-rules/test/sia-r3/rule.spec.tsx @@ -0,0 +1,85 @@ +import { h } from "@siteimprove/alfa-dom"; +import { test } from "@siteimprove/alfa-test"; + +import R3, { Outcomes } from "../../src/sia-r3/rule"; + +import { evaluate } from "../common/evaluate"; +import { passed, failed, inapplicable } from "../common/outcome"; + +test("evaluate() passes a single id attribute.", async (t) => { + const target = <div id="my-div">This is my first element</div>; + + const document = h.document([target]); + + t.deepEqual(await evaluate(R3, { document }), [ + passed(R3, target, { + 1: Outcomes.HasUniqueId, + }), + ]); +}); + +test("evaluate() passes multiple unique id attributes", async (t) => { + const target1 = <div id="my-div1">This is my first element</div>; + const target2 = <div id="my-div2">This is my second element</div>; + const target3 = <svg id="my-div3">This is my third element</svg>; + + const document = h.document([target1, target2, target3]); + + t.deepEqual(await evaluate(R3, { document }), [ + passed(R3, target1, { + 1: Outcomes.HasUniqueId, + }), + passed(R3, target2, { + 1: Outcomes.HasUniqueId, + }), + passed(R3, target3, { + 1: Outcomes.HasUniqueId, + }), + ]); +}); + +test("evaluate() fails duplicated id attributes", async (t) => { + const target1 = <div id="label">Name</div>; + const target2 = <div id="label">City</div>; + + const document = h.document([target1, target2]); + + t.deepEqual(await evaluate(R3, { document }), [ + failed(R3, target1, { + 1: Outcomes.HasNonUniqueId, + }), + failed(R3, target2, { + 1: Outcomes.HasNonUniqueId, + }), + ]); +}); + +test("evaluate() fails duplicated id attributes on SVG element", async (t) => { + const target1 = <div id="label">Name</div>; + const target2 = ( + <svg id="label"> + <text x="0" y="15"> + City + </text> + </svg> + ); + + const document = h.document([target1, target2]); + + t.deepEqual(await evaluate(R3, { document }), [ + failed(R3, target1, { + 1: Outcomes.HasNonUniqueId, + }), + failed(R3, target2, { + 1: Outcomes.HasNonUniqueId, + }), + ]); +}); + +test("evaluate() is inapplicable to a document without id attribute", async (t) => { + const target1 = <div>This is my first element</div>; + + const document = h.document([target1]); + + t.deepEqual(await evaluate(R3, { document }), [inapplicable(R3)]); +}); diff --git a/packages/alfa-rules/test/sia-r38/rule.spec.tsx b/packages/alfa-rules/test/sia-r38/rule.spec.tsx index dcae0eb4ab..6392888c44 100644 --- a/packages/alfa-rules/test/sia-r38/rule.spec.tsx +++ b/packages/alfa-rules/test/sia-r38/rule.spec.tsx @@ -1,6 +1,6 @@ +import { h } from "@siteimprove/alfa-dom"; import { test } from "@siteimprove/alfa-test"; -import { Document } from "@siteimprove/alfa-dom"; import { None } from "@siteimprove/alfa-option"; import R38, { Outcomes } from "../../src/sia-r38/rule"; @@ -17,7 +17,7 @@ test("evaluate() passes when some atomic rules are passing", async (t) => { </video> ); - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual( await evaluate( @@ -50,7 +50,7 @@ test("evaluate() can't tell when there are not enough answers to expectation", a </video> ); - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual( await evaluate( diff --git a/packages/alfa-rules/test/sia-r4/rule.spec.tsx b/packages/alfa-rules/test/sia-r4/rule.spec.tsx new file mode 100644 index 0000000000..88f6e2a4f5 --- /dev/null +++ b/packages/alfa-rules/test/sia-r4/rule.spec.tsx @@ -0,0 +1,66 @@ +import { h } from "@siteimprove/alfa-dom"; +import { test } from "@siteimprove/alfa-test"; + +import R4, { Outcomes } from "../../src/sia-r4/rule"; + +import { evaluate } from "../common/evaluate"; +import { passed, failed, inapplicable } from "../common/outcome"; + +test(`evaluate() passes an html element with lang attribute which has a + non-empty ("") value`, async (t) => { + const target = <html lang="en"></html>; + + const document = h.document([target]); + + t.deepEqual(await evaluate(R4, { document }), [ + passed(R4, target, { + 1: Outcomes.HasLanguage, + }), + ]); +}); + +test(`evaluate() fails an html element with no lang attribute.`, async (t) => { + const target = <html></html>; + + const document = h.document([target]); + + t.deepEqual(await evaluate(R4, { document }), [ + failed(R4, target, { + 1: Outcomes.HasNoLanguage, + }), + ]); +}); + +test(`evaluate() fails an html element with lang attribute whose value is + only ASCII whitespace`, async (t) => { + const target = <html lang=" "></html>; + + const document = h.document([target]); + + t.deepEqual(await evaluate(R4, { document }), [ + failed(R4, target, { + 1: Outcomes.HasNoLanguage, + }), + ]); +}); + +test(`evaluate() fails an html element with only an xml:lang attribute.`, async (t) => { + const attribute = h.attribute("xml:lang", "en"); + const target = h.element("html", [attribute]); + + const document = h.document([target]); + + t.deepEqual(await evaluate(R4, { document }), [ + failed(R4, target, { + 1: Outcomes.HasNoLanguage, + }), + ]); +}); + +test(`evaluate() is inapplicable to svg element.`, async (t) => { + const target = <svg xmlns="http://www.w3.org/2000/svg"></svg>; + + const document = h.document([target]); + + t.deepEqual(await evaluate(R4, { document }), [inapplicable(R4)]); +}); diff --git a/packages/alfa-rules/test/sia-r41/rule.spec.tsx b/packages/alfa-rules/test/sia-r41/rule.spec.tsx index ec29a438c7..82bc0c4e35 100644 --- a/packages/alfa-rules/test/sia-r41/rule.spec.tsx +++ b/packages/alfa-rules/test/sia-r41/rule.spec.tsx @@ -1,7 +1,6 @@ +import { h } from "@siteimprove/alfa-dom"; import { test } from "@siteimprove/alfa-test"; -import { Document } from "@siteimprove/alfa-dom"; - import R41, { Outcomes } from "../../src/sia-r41/rule"; import { Group } from "../../src/common/group"; @@ -16,7 +15,7 @@ test(`evaluate() passes two links that have the same name and reference the same resource`, async (t) => { const target = [<a href="foo.html">Foo</a>, <a href="foo.html">Foo</a>]; - const document = Document.of(target); + const document = h.document(target); t.deepEqual(await evaluate(R41, { document }), [ passed(R41, Group.of(target), { @@ -29,7 +28,7 @@ test(`evaluate() fails two links that have the same name, but reference different resources`, async (t) => { const target = [<a href="foo.html">Foo</a>, <a href="bar.html">Foo</a>]; - const document = Document.of(target); + const document = h.document(target); t.deepEqual( await evaluate( @@ -51,7 +50,7 @@ test(`evaluate() passes two links that have the same name and reference equivalent resources`, async (t) => { const target = [<a href="foo.html">Foo</a>, <a href="bar.html">Foo</a>]; - const document = Document.of(target); + const document = h.document(target); t.deepEqual( await evaluate( @@ -70,7 +69,7 @@ test(`evaluate() passes two links that have the same name and reference }); test(`evaluate() is inapplicable to two links that have different names`, async (t) => { - const document = Document.of([ + const document = h.document([ <a href="foo.html">Foo</a>, <a href="bar.html">Bar</a>, ]); @@ -88,7 +87,7 @@ test("evaluate() correctly resolves relative URLs", async (t) => { <a href="../to/foo.html">Foo</a>, ]; - const document = Document.of(target); + const document = h.document(target); t.deepEqual( await evaluate(R41, { diff --git a/packages/alfa-rules/test/sia-r42/rule.spec.tsx b/packages/alfa-rules/test/sia-r42/rule.spec.tsx new file mode 100644 index 0000000000..6b671d5a3b --- /dev/null +++ b/packages/alfa-rules/test/sia-r42/rule.spec.tsx @@ -0,0 +1,144 @@ +import { h } from "@siteimprove/alfa-dom"; +import { test } from "@siteimprove/alfa-test"; + +import R42, { Outcomes } from "../../src/sia-r42/rule"; + +import { evaluate } from "../common/evaluate"; +import { passed, failed, inapplicable } from "../common/outcome"; + +test(`evaluates() passes an implicit listitem inside a list`, async (t) => { + const target = <li>Foo</li>; + + const document = h.document([<ul>{target}</ul>]); + + t.deepEqual(await evaluate(R42, { document }), [ + passed(R42, target, { + 1: Outcomes.IsOwnedByContextRole, + }), + ]); +}); + +test(`evaluates() passes an explicit listitem inside a list`, async (t) => { + const target = <div role="listitem">Foo</div>; + + const document = h.document([<div role="list">{target}</div>]); + + t.deepEqual(await evaluate(R42, { document }), [ + passed(R42, target, { + 1: Outcomes.IsOwnedByContextRole, + }), + ]); +}); + +test(`evaluates() fails an orphaned listitem`, async (t) => { + const target = <div role="listitem">Foo</div>; + + const document = h.document([target]); + + t.deepEqual(await evaluate(R42, { document }), [ + failed(R42, target, { + 1: Outcomes.IsNotOwnedByContextRole, + }), + ]); +}); + +test(`evaluates() skips through container nodes`, async (t) => { + const target = <div role="listitem">Foo</div>; + + const document = h.document([ + <div role="list"> + <div>{target}</div> + </div>, + ]); + + t.deepEqual(await evaluate(R42, { document }), [ + passed(R42, target, { + 1: Outcomes.IsOwnedByContextRole, + }), + ]); +}); + +test(`evaluates() fails listitem in intermediate non-container nodes`, async (t) => { + const target = <div role="listitem">Foo</div>; + + const document = h.document([ + <div role="list"> + <div role="menu">{target}</div> + </div>, + ]); + + t.deepEqual(await evaluate(R42, { document }), [ + failed(R42, target, { + 1: Outcomes.IsNotOwnedByContextRole, + }), + ]); +}); + +test(`evaluates() follows \`aria-owns\``, async (t) => { + const target = ( + <div id="target" role="listitem"> + Foo + </div> + ); + + const document = h.document([ + <div role="list" aria-owns="target"></div>, + target, + ]); + + t.deepEqual(await evaluate(R42, { document }), [ + passed(R42, target, { + 1: Outcomes.IsOwnedByContextRole, + }), + ]); +}); + +test(`evaluates() passes a \`row\` inside a \`rowgroup\` inside a \`table\``, async (t) => { + const cell = <td>Foo</td>; + const row = <tr>{cell}</tr>; + const rowGroup = <tbody>{row}</tbody>; + + const document = h.document([<table>{rowGroup}</table>]); + + t.deepEqual(await evaluate(R42, { document }), [ + passed(R42, rowGroup, { + 1: Outcomes.IsOwnedByContextRole, + }), + passed(R42, row, { + 1: Outcomes.IsOwnedByContextRole, + }), + passed(R42, cell, { + 1: Outcomes.IsOwnedByContextRole, + }), + ]); +}); + +test(`evaluates() fails a \`row\` inside an orphaned \`rowgroup\``, async (t) => { + const row = <div role="row">Foo</div>; + const rowGroup = <div role="rowgroup">{row}</div>; + + const document = h.document([rowGroup]); + + t.deepEqual(await evaluate(R42, { document }), [ + failed(R42, rowGroup, { + 1: Outcomes.IsNotOwnedByContextRole, + }), + failed(R42, row, { + 1: Outcomes.IsNotOwnedByContextRole, + }), + ]); +}); + +test(`evaluates() is inapplicable when on element whose role has no required parents`, async (t) => { + const document = h.document([<h1>Header</h1>]); + + t.deepEqual(await evaluate(R42, { document }), [inapplicable(R42)]); +}); + +test(`evaluates() is inapplicable on element that is not in the accessiblity tree`, async (t) => { + const document = h.document([ + <div role="listitem" style={{ display: "none" }}></div>, + ]); + + t.deepEqual(await evaluate(R42, { document }), [inapplicable(R42)]); +}); diff --git a/packages/alfa-rules/test/sia-r45/rule.spec.tsx b/packages/alfa-rules/test/sia-r45/rule.spec.tsx index 31c01e08e4..9dd821bf1e 100644 --- a/packages/alfa-rules/test/sia-r45/rule.spec.tsx +++ b/packages/alfa-rules/test/sia-r45/rule.spec.tsx @@ -1,8 +1,6 @@ -import { h } from "@siteimprove/alfa-dom/h"; +import { h } from "@siteimprove/alfa-dom"; import { test } from "@siteimprove/alfa-test"; -import { Document } from "@siteimprove/alfa-dom"; - import R45, { Outcomes } from "../../src/sia-r45/rule"; import { evaluate } from "../common/evaluate"; @@ -12,7 +10,7 @@ test(`evaluate() passes when tokens in headers list refer to cells in the same table`, async (t) => { const target = h.attribute("headers", "header1 header2"); - const document = Document.of([ + const document = h.document([ <table> <thead> <tr> @@ -38,7 +36,7 @@ test(`evaluate() fails when some tokens in headers list do not refer to cells in the same table`, async (t) => { const target = h.attribute("headers", "header1 header2"); - const document = Document.of([ + const document = h.document([ <table> <thead> <tr> @@ -64,7 +62,7 @@ test(`evaluate() fails when some token in the headers list refer to the cell itself`, async (t) => { const target = h.attribute("headers", "header cell"); - const document = Document.of([ + const document = h.document([ <table> <thead> <tr> @@ -93,7 +91,7 @@ test(`evaluate() fails when some token in the headers list refer to the cell }); test("evaluate() is inapplicable to a table without headers attributes", async (t) => { - const document = Document.of([ + const document = h.document([ <table> <thead> <tr> @@ -113,7 +111,7 @@ test("evaluate() is inapplicable to a table without headers attributes", async ( }); test("evaluate() is inapplicable to a table which is not included in the accessiblity tree", async (t) => { - const document = Document.of([ + const document = h.document([ <table aria-hidden="true"> <td headers="foo">Bar</td> </table>, @@ -123,7 +121,7 @@ test("evaluate() is inapplicable to a table which is not included in the accessi }); test("evaluate() is inapplicable to a table with a presentational role", async (t) => { - const document = Document.of([ + const document = h.document([ <table role="presentation"> <td headers="foo">Bar</td> </table>, diff --git a/packages/alfa-rules/test/sia-r46/rule.spec.tsx b/packages/alfa-rules/test/sia-r46/rule.spec.tsx index 374a3b0b45..96a3ac9301 100644 --- a/packages/alfa-rules/test/sia-r46/rule.spec.tsx +++ b/packages/alfa-rules/test/sia-r46/rule.spec.tsx @@ -1,7 +1,6 @@ +import { h } from "@siteimprove/alfa-dom"; import { test } from "@siteimprove/alfa-test"; -import { Document } from "@siteimprove/alfa-dom"; - import R46, { Outcomes } from "../../src/sia-r46/rule"; import { evaluate } from "../common/evaluate"; @@ -10,7 +9,7 @@ import { passed, failed, inapplicable } from "../common/outcome"; test("evaluate() passes on explicit header", async (t) => { const target = <th>Time</th>; - const document = Document.of([ + const document = h.document([ <table> <tr>{target}</tr> <tr> @@ -30,7 +29,7 @@ test("evaluate() passes on implicit headers", async (t) => { const target1 = <th id="col1">Column 1</th>; const target2 = <th id="col2">Column 2</th>; - const document = Document.of([ + const document = h.document([ <table> <tr> {target1} @@ -59,7 +58,7 @@ test("evaluate() fails on headers with no data cell", async (t) => { const target1 = <th>Column 1</th>; const target2 = <th>Column 2</th>; - const document = Document.of([ + const document = h.document([ <table> <tr> {target1} @@ -83,7 +82,7 @@ test("evaluate() fails on headers with no data cell", async (t) => { }); test("evaluate() is inapplicable if there is no header cell", async (t) => { - const document = Document.of([ + const document = h.document([ <table> <tr> <th role="cell">Column A</th> @@ -98,7 +97,7 @@ test("evaluate() is inapplicable if there is no header cell", async (t) => { }); test("evaluate() is inapplicable if the table element is ignored", async (t) => { - const document = Document.of([ + const document = h.document([ <table role="presentation"> <tr> <th>Column A</th> @@ -117,7 +116,7 @@ test("evaluate() passes headers assigned only to other headers", async (t) => { const target2 = <th>Column header</th>; const target3 = <th>Row header</th>; - const document = Document.of([ + const document = h.document([ <table> <tr> {target1} diff --git a/packages/alfa-rules/test/sia-r5/rule.spec.tsx b/packages/alfa-rules/test/sia-r5/rule.spec.tsx new file mode 100644 index 0000000000..f901a4dc08 --- /dev/null +++ b/packages/alfa-rules/test/sia-r5/rule.spec.tsx @@ -0,0 +1,51 @@ +import { h } from "@siteimprove/alfa-dom"; +import { test } from "@siteimprove/alfa-test"; + +import R5, { Outcomes } from "../../src/sia-r5/rule"; + +import { evaluate } from "../common/evaluate"; +import { passed, failed, inapplicable } from "../common/outcome"; + +test(`evaluate() passes a lang attribute with valid primary tag`, async (t) => { + const html = <html lang="en"></html>; + + const document = h.document([html]); + + t.deepEqual(await evaluate(R5, { document }), [ + passed(R5, html.attribute("lang").get(), { + 1: Outcomes.HasValidLanguage, + }), + ]); +}); + +test(`evaluate() passes a lang attribute with valid primary tag and + invalid region subtag`, async (t) => { + const html = <html lang="en-US-GB"></html>; + + const document = h.document([html]); + + t.deepEqual(await evaluate(R5, { document }), [ + passed(R5, html.attribute("lang").get(), { + 1: Outcomes.HasValidLanguage, + }), + ]); +}); + +test(`evaluate() fails a lang attribute with invalid primary tag.`, async (t) => { + const html = <html lang="invalid"></html>; + + const document = h.document([html]); + + t.deepEqual(await evaluate(R5, { document }), [ + failed(R5, html.attribute("lang").get(), { + 1: Outcomes.HasNoValidLanguage, + }), + ]); +}); + +test(`evaluate() is inapplicable to svg elements.`, async (t) => { + const html = <svg xmlns="http://www.w3.org/2000/svg" lang="fr"></svg>; + const document = h.document([html]); + + t.deepEqual(await evaluate(R5, { document }), [inapplicable(R5)]); +}); diff --git a/packages/alfa-rules/test/sia-r53/rule.spec.tsx b/packages/alfa-rules/test/sia-r53/rule.spec.tsx index 31d80a9bf3..61d7d80c2b 100644 --- a/packages/alfa-rules/test/sia-r53/rule.spec.tsx +++ b/packages/alfa-rules/test/sia-r53/rule.spec.tsx @@ -1,7 +1,6 @@ +import { h } from "@siteimprove/alfa-dom"; import { test } from "@siteimprove/alfa-test"; -import { Document } from "@siteimprove/alfa-dom"; - import R53, { Outcomes } from "../../src/sia-r53/rule"; import { evaluate } from "../common/evaluate"; @@ -11,7 +10,7 @@ test("evaluate() passes when the document headings are structured", async (t) => const target1 = <h2>Chapter one</h2>; const target2 = <h3>Section one</h3>; - const document = Document.of([ + const document = h.document([ <html> <h1>Part one</h1> {target1} @@ -34,7 +33,7 @@ test("evaluate() fails when the document headings are not properly structured", const target2 = <h2>Part two</h2>; const target3 = <h6>Chapter one</h6>; - const document = Document.of([ + const document = h.document([ <html> <h1>Part one</h1> {target1} @@ -57,7 +56,7 @@ test("evaluate() fails when the document headings are not properly structured", }); test("evaluate() is inapplicable when the document has only one heading", async (t) => { - const document = Document.of([ + const document = h.document([ <html> <h1>Lone heading</h1> </html>, @@ -70,7 +69,7 @@ test("evaluate() ignore headings that are not exposed", async (t) => { const target1 = <h2>Chapter one</h2>; const target2 = <h2>Chapter two</h2>; - const document = Document.of([ + const document = h.document([ <html> <h1>Part one</h1> <h3 hidden>I'm not here</h3> diff --git a/packages/alfa-rules/test/sia-r56/rule.spec.tsx b/packages/alfa-rules/test/sia-r56/rule.spec.tsx new file mode 100644 index 0000000000..d787f212a1 --- /dev/null +++ b/packages/alfa-rules/test/sia-r56/rule.spec.tsx @@ -0,0 +1,54 @@ +import { h } from "@siteimprove/alfa-dom"; +import { test } from "@siteimprove/alfa-test"; + +import R56, { Outcomes } from "../../src/sia-r56/rule"; + +import { evaluate } from "../common/evaluate"; +import { passed, failed, inapplicable } from "../common/outcome"; + +import { Group } from "../../src/common/group"; + +test("evaluate() passes when same landmarks have different names", async (t) => { + const author = <aside aria-label="About the author" id="author" />; + const book = <aside aria-label="About the book" id="book" />; + + const document = h.document([author, book]); + const target = Group.of([author, book]); + + t.deepEqual(await evaluate(R56, { document }), [ + passed(R56, target, { 1: Outcomes.differentNames("complementary") }), + ]); +}); + +test("evaluate() fails when same landmarks have same names", async (t) => { + const aside1 = <aside aria-label="More information" id="author" />; + const aside2 = <aside aria-label="More information" id="book" />; + + const document = h.document([aside1, aside2]); + const target = Group.of([aside1, aside2]); + + t.deepEqual(await evaluate(R56, { document }), [ + failed(R56, target, { 1: Outcomes.sameNames("complementary", [target]) }), + ]); +}); + +test("evaluate() fails when same landmarks have no names", async (t) => { + const aside1 = <aside id="author" />; + const aside2 = <aside id="book" />; + + const document = h.document([aside1, aside2]); + const target = Group.of([aside1, aside2]); + + t.deepEqual(await evaluate(R56, { document }), [ + failed(R56, target, { 1: Outcomes.sameNames("complementary", [target]) }), + ]); +}); + +test("evaluate() is inapplicable when only different landmarks exist", async (t) => { + const aside = <aside />; + const nav = <nav />; + + const document = h.document([aside, nav]); + + t.deepEqual(await evaluate(R56, { document }), [inapplicable(R56)]); +}); diff --git a/packages/alfa-rules/test/sia-r57/rule.spec.tsx b/packages/alfa-rules/test/sia-r57/rule.spec.tsx index 91b7b6ad30..a1169ed57a 100644 --- a/packages/alfa-rules/test/sia-r57/rule.spec.tsx +++ b/packages/alfa-rules/test/sia-r57/rule.spec.tsx @@ -1,8 +1,6 @@ -import { h } from "@siteimprove/alfa-dom/h"; +import { h } from "@siteimprove/alfa-dom"; import { test } from "@siteimprove/alfa-test"; -import { Document } from "@siteimprove/alfa-dom"; - import R57, { Outcomes } from "../../src/sia-r57/rule"; import { evaluate } from "../common/evaluate"; @@ -11,7 +9,7 @@ import { passed, failed, inapplicable } from "../common/outcome"; test("evaluate() passes a text node that is included in a landmark", async (t) => { const target = h.text("This text is included in a landmark"); - const document = Document.of([<main>{target}</main>]); + const document = h.document([<main>{target}</main>]); t.deepEqual(await evaluate(R57, { document }), [ passed(R57, target, { @@ -23,7 +21,7 @@ test("evaluate() passes a text node that is included in a landmark", async (t) = test("evaluate() fails a text node that is not included in a landmark", async (t) => { const target = h.text("This text is not included in a landmark"); - const document = Document.of([<div>{target}</div>, <main />]); + const document = h.document([<div>{target}</div>, <main />]); t.deepEqual(await evaluate(R57, { document }), [ failed(R57, target, { @@ -33,7 +31,7 @@ test("evaluate() fails a text node that is not included in a landmark", async (t }); test("evaluate() is not applicable to text nodes not in the accessibility tree", async (t) => { - const document = Document.of([ + const document = h.document([ <div hidden>This text is not in the accessibility tree</div>, <main />, ]); @@ -42,7 +40,7 @@ test("evaluate() is not applicable to text nodes not in the accessibility tree", }); test("evaluate() is not applicable when no landmarks are found", async (t) => { - const document = Document.of([ + const document = h.document([ <div>This text is in the accessibility tree</div>, ]); @@ -50,19 +48,19 @@ test("evaluate() is not applicable when no landmarks are found", async (t) => { }); test("evaluate() is not applicable to empty text nodes", async (t) => { - const document = Document.of([<div>{h.text("")}</div>]); + const document = h.document([<div>{h.text("")}</div>]); t.deepEqual(await evaluate(R57, { document }), [inapplicable(R57)]); }); test("evaluate() is not applicable to text nodes with only whitespace", async (t) => { - const document = Document.of([<div>{h.text(" \u00a0")}</div>]); + const document = h.document([<div>{h.text(" \u00a0")}</div>]); t.deepEqual(await evaluate(R57, { document }), [inapplicable(R57)]); }); test("evaluate() is not applicable to descendants of an <iframe> element", async (t) => { - const document = Document.of([ + const document = h.document([ <iframe> <span>Hello</span> world </iframe>, diff --git a/packages/alfa-rules/test/sia-r6/rule.spec.tsx b/packages/alfa-rules/test/sia-r6/rule.spec.tsx new file mode 100644 index 0000000000..648d42bce8 --- /dev/null +++ b/packages/alfa-rules/test/sia-r6/rule.spec.tsx @@ -0,0 +1,90 @@ +import { h } from "@siteimprove/alfa-dom"; +import { test } from "@siteimprove/alfa-test"; + +import R6, { Outcomes } from "../../src/sia-r6/rule"; + +import { evaluate } from "../common/evaluate"; +import { passed, failed, inapplicable } from "../common/outcome"; + +const html = (lang: string, xml: string) => + h.element("html", [h.attribute("lang", lang), h.attribute("xml:lang", xml)]); + +test(`evaluate() passes an html element which has identical primary + language subtags for its lang and xml:lang attributes`, async (t) => { + const target = html("en", "en"); + + const document = h.document([target]); + + t.deepEqual(await evaluate(R6, { document }), [ + passed(R6, target, { + 1: Outcomes.HasMatchingLanguages, + }), + ]); +}); + +test(`evaluate() passes an html element which has identical primary and + extended language subtags for its lang and xml:lang attributes`, async (t) => { + const target = html("en-GB", "en-GB"); + + const document = h.document([target]); + + t.deepEqual(await evaluate(R6, { document }), [ + passed(R6, target, { + 1: Outcomes.HasMatchingLanguages, + }), + ]); +}); + +test(`evaluate() fails an html element which has different primary + language subtags for its lang and xml:lang attributes`, async (t) => { + const target = html("fr", "en"); + + const document = h.document([target]); + + t.deepEqual(await evaluate(R6, { document }), [ + failed(R6, target, { + 1: Outcomes.HasNonMatchingLanguages, + }), + ]); +}); + +test(`evaluate() fails an html element which has different primary language + subtags but matching extended subtags for its lang and xml:lang attributes`, async (t) => { + const target = html("fr-CA", "en-CA"); + + const document = h.document([target]); + + t.deepEqual(await evaluate(R6, { document }), [ + failed(R6, target, { + 1: Outcomes.HasNonMatchingLanguages, + }), + ]); +}); + +test(`evaluate() is inapplicable to svg elements`, async (t) => { + const target = h.element("svg", [ + h.attribute("lang", "en"), + h.attribute("xml:lang", "en"), + ]); + + const document = h.document([target]); + + t.deepEqual(await evaluate(R6, { document }), [inapplicable(R6)]); +}); + +test(`evaluate() is inapplicable to html elements whose lang attribute is not + a valid language tag`, async (t) => { + const target = html("invalid", "en"); + + const document = h.document([target]); + + t.deepEqual(await evaluate(R6, { document }), [inapplicable(R6)]); +}); + +test(`evaluate() is inapplicable to html elements with an empty xml:lang attribute`, async (t) => { + const target = html("fr", ""); + + const document = h.document([target]); + + t.deepEqual(await evaluate(R6, { document }), [inapplicable(R6)]); +}); diff --git a/packages/alfa-rules/test/sia-r61/rule.spec.tsx b/packages/alfa-rules/test/sia-r61/rule.spec.tsx index 694b1cbf8f..b1e806a142 100644 --- a/packages/alfa-rules/test/sia-r61/rule.spec.tsx +++ b/packages/alfa-rules/test/sia-r61/rule.spec.tsx @@ -1,14 +1,13 @@ +import { h } from "@siteimprove/alfa-dom"; import { test } from "@siteimprove/alfa-test"; -import { Document } from "@siteimprove/alfa-dom"; - import R61, { Outcomes } from "../../src/sia-r61/rule"; import { evaluate } from "../common/evaluate"; import { passed, failed, inapplicable } from "../common/outcome"; test("evaluate() passes when the document starts with an explicit level 1 heading", async (t) => { - const document = Document.of([ + const document = h.document([ <html> <div role="heading" aria-level="1"> Prefer using heading elements! @@ -24,7 +23,7 @@ test("evaluate() passes when the document starts with an explicit level 1 headin }); test("evaluate() passes when the document starts with an implicit level 1 heading", async (t) => { - const document = Document.of([ + const document = h.document([ <html> <h1>Semantic HTML is good</h1> </html>, @@ -38,7 +37,7 @@ test("evaluate() passes when the document starts with an implicit level 1 headin }); test("evaluate() fails when the document starts with a level 4 heading", async (t) => { - const document = Document.of([ + const document = h.document([ <html> <h4>Semantic HTML is good</h4> </html>, @@ -52,7 +51,7 @@ test("evaluate() fails when the document starts with a level 4 heading", async ( }); test("evaluate() is inapplicable when there is no heading", async (t) => { - const document = Document.of([ + const document = h.document([ <html> <p>Hello World!</p> </html>, @@ -62,7 +61,7 @@ test("evaluate() is inapplicable when there is no heading", async (t) => { }); test("evaluate() skips headings that are not exposed to assistive technologies", async (t) => { - const document = Document.of([ + const document = h.document([ <html> <h2 aria-hidden="true">Now you can't see me</h2> <h1>Now you can.</h1> diff --git a/packages/alfa-rules/test/sia-r62/rule.spec.tsx b/packages/alfa-rules/test/sia-r62/rule.spec.tsx index b0ec286f55..031cce100c 100644 --- a/packages/alfa-rules/test/sia-r62/rule.spec.tsx +++ b/packages/alfa-rules/test/sia-r62/rule.spec.tsx @@ -1,23 +1,60 @@ -import { h } from "@siteimprove/alfa-dom/h"; +import { h } from "@siteimprove/alfa-dom"; +import { Err, Ok } from "@siteimprove/alfa-result"; +import { Property } from "@siteimprove/alfa-style"; import { test } from "@siteimprove/alfa-test"; -import { Document } from "@siteimprove/alfa-dom"; - -import R62, { Outcomes } from "../../src/sia-r62/rule"; +import R62, { ComputedStyles, Outcomes } from "../../src/sia-r62/rule"; import { evaluate } from "../common/evaluate"; import { passed, failed, inapplicable } from "../common/outcome"; +// default styling of links +// The initial value of border-top is medium, resolving as 3px. However, when +// computing and border-style is none, this is computed as 0px. +// As a consequence, even without changing `border` at all, the computed value +// of border-top is not equal to its initial value and needs to expressed here! +// +// Confused? Wait, same joke happens for outline-width except that now on focus +// outline-style is not none, so the computed value of outline-width is its +// initial value. As a consequence, we cannot just override properties since +// in this case we need to actually *remove* outline-width from the diagnostic! +const defaultProperties: Array< + [Property.Name | Property.Shorthand.Name, string] +> = [ + ["border-width", "0px"], + ["color", "rgb(0% 0% 93.33333%)"], + ["text-decoration", "underline"], + ["outline", "0px"], +]; +const focusProperties: Array< + [Property.Name | Property.Shorthand.Name, string] +> = [ + ["border-width", "0px"], + ["color", "rgb(0% 0% 93.33333%)"], + ["outline", "auto"], + ["text-decoration", "underline"], +]; +const noDistinguishingProperties: Array< + [Property.Name | Property.Shorthand.Name, string] +> = [ + ["border-width", "0px"], + ["color", "rgb(0% 0% 93.33333%)"], + ["outline", "0px"], +]; + +const defaultStyle = Ok.of(ComputedStyles.of(defaultProperties)); +const focusStyle = Ok.of(ComputedStyles.of(focusProperties)); +const noStyle = Err.of(ComputedStyles.of(noDistinguishingProperties)); + test(`evaluate() passes an <a> element with a <p> parent element with non-link text content`, async (t) => { const target = <a href="#">Link</a>; - const document = Document.of([<p>Hello {target}</p>]); + const document = h.document([<p>Hello {target}</p>]); t.deepEqual(await evaluate(R62, { document }), [ passed(R62, target, { - 1: Outcomes.IsDistinguishable, - 2: Outcomes.IsDistinguishableWhenVisited, + 1: Outcomes.IsDistinguishable(defaultStyle, defaultStyle, focusStyle), }), ]); }); @@ -26,7 +63,7 @@ test(`evaluate() passes an <a> element with a <p> parent element with non-link text content in a <span> child element`, async (t) => { const target = <a href="#">Link</a>; - const document = Document.of([ + const document = h.document([ <p> <span>Hello</span> {target} </p>, @@ -34,8 +71,7 @@ test(`evaluate() passes an <a> element with a <p> parent element with non-link t.deepEqual(await evaluate(R62, { document }), [ passed(R62, target, { - 1: Outcomes.IsDistinguishable, - 2: Outcomes.IsDistinguishableWhenVisited, + 1: Outcomes.IsDistinguishable(defaultStyle, defaultStyle, focusStyle), }), ]); }); @@ -44,11 +80,12 @@ test(`evaluate() fails an <a> element that removes the default text decoration without replacing it with another distinguishing feature`, async (t) => { const target = <a href="#">Link</a>; - const document = Document.of( + const document = h.document( [<p>Hello {target}</p>], [ h.sheet([ h.rule.style("a", { + outline: "none", textDecoration: "none", }), ]), @@ -57,8 +94,7 @@ test(`evaluate() fails an <a> element that removes the default text decoration t.deepEqual(await evaluate(R62, { document }), [ failed(R62, target, { - 1: Outcomes.IsNotDistinguishable, - 2: Outcomes.IsNotDistinguishableWhenVisited, + 1: Outcomes.IsNotDistinguishable(noStyle, noStyle, noStyle), }), ]); }); @@ -67,7 +103,7 @@ test(`evaluate() fails an <a> element that removes the default text decoration on hover without replacing it with another distinguishing feature`, async (t) => { const target = <a href="#">Link</a>; - const document = Document.of( + const document = h.document( [<p>Hello {target}</p>], [ h.sheet([ @@ -80,8 +116,7 @@ test(`evaluate() fails an <a> element that removes the default text decoration t.deepEqual(await evaluate(R62, { document }), [ failed(R62, target, { - 1: Outcomes.IsNotDistinguishable, - 2: Outcomes.IsNotDistinguishableWhenVisited, + 1: Outcomes.IsNotDistinguishable(defaultStyle, noStyle, focusStyle), }), ]); }); @@ -91,7 +126,7 @@ test(`evaluate() fails an <a> element that removes the default text decoration distinguishing feature`, async (t) => { const target = <a href="#">Link</a>; - const document = Document.of( + const document = h.document( [<p>Hello {target}</p>], [ h.sheet([ @@ -105,8 +140,7 @@ test(`evaluate() fails an <a> element that removes the default text decoration t.deepEqual(await evaluate(R62, { document }), [ failed(R62, target, { - 1: Outcomes.IsNotDistinguishable, - 2: Outcomes.IsNotDistinguishableWhenVisited, + 1: Outcomes.IsNotDistinguishable(defaultStyle, defaultStyle, noStyle), }), ]); }); @@ -116,16 +150,13 @@ test(`evaluate() fails an <a> element that removes the default text decoration distinguishing feature`, async (t) => { const target = <a href="#">Link</a>; - const document = Document.of( + const document = h.document( [<p>Hello {target}</p>], [ h.sheet([ - h.rule.style("a:focus", { - outline: "none", - }), - h.rule.style("a:hover, a:focus", { textDecoration: "none", + outline: "none", }), ]), ] @@ -133,8 +164,7 @@ test(`evaluate() fails an <a> element that removes the default text decoration t.deepEqual(await evaluate(R62, { document }), [ failed(R62, target, { - 1: Outcomes.IsNotDistinguishable, - 2: Outcomes.IsNotDistinguishableWhenVisited, + 1: Outcomes.IsNotDistinguishable(defaultStyle, noStyle, noStyle), }), ]); }); @@ -143,11 +173,12 @@ test(`evaluate() fails an <a> element that applies a text decoration only on hover`, async (t) => { const target = <a href="#">Link</a>; - const document = Document.of( + const document = h.document( [<p>Hello {target}</p>], [ h.sheet([ h.rule.style("a", { + outline: "none", textDecoration: "none", }), @@ -160,8 +191,7 @@ test(`evaluate() fails an <a> element that applies a text decoration only on t.deepEqual(await evaluate(R62, { document }), [ failed(R62, target, { - 1: Outcomes.IsNotDistinguishable, - 2: Outcomes.IsNotDistinguishableWhenVisited, + 1: Outcomes.IsNotDistinguishable(noStyle, defaultStyle, noStyle), }), ]); }); @@ -170,11 +200,12 @@ test(`evaluate() fails an <a> element that applies a text decoration only on focus`, async (t) => { const target = <a href="#">Link</a>; - const document = Document.of( + const document = h.document( [<p>Hello {target}</p>], [ h.sheet([ h.rule.style("a", { + outline: "none", textDecoration: "none", }), @@ -187,8 +218,7 @@ test(`evaluate() fails an <a> element that applies a text decoration only on t.deepEqual(await evaluate(R62, { document }), [ failed(R62, target, { - 1: Outcomes.IsNotDistinguishable, - 2: Outcomes.IsNotDistinguishableWhenVisited, + 1: Outcomes.IsNotDistinguishable(noStyle, noStyle, defaultStyle), }), ]); }); @@ -197,11 +227,12 @@ test(`evaluate() fails an <a> element that applies a text decoration only on hover and focus`, async (t) => { const target = <a href="#">Link</a>; - const document = Document.of( + const document = h.document( [<p>Hello {target}</p>], [ h.sheet([ h.rule.style("a", { + outline: "none", textDecoration: "none", }), @@ -214,8 +245,7 @@ test(`evaluate() fails an <a> element that applies a text decoration only on t.deepEqual(await evaluate(R62, { document }), [ failed(R62, target, { - 1: Outcomes.IsNotDistinguishable, - 2: Outcomes.IsNotDistinguishableWhenVisited, + 1: Outcomes.IsNotDistinguishable(noStyle, defaultStyle, defaultStyle), }), ]); }); @@ -224,7 +254,7 @@ test(`evaluate() passes an applicable <a> element that removes the default text decoration and instead applies an outline`, async (t) => { const target = <a href="#">Link</a>; - const document = Document.of( + const document = h.document( [<p>Hello {target}</p>], [ h.sheet([ @@ -236,10 +266,17 @@ test(`evaluate() passes an applicable <a> element that removes the default text ] ); + const styles = Ok.of( + ComputedStyles.of([ + ["border-width", "0px"], + ["color", "rgb(0% 0% 93.33333%)"], + ["outline", "auto"], + ]) + ); + t.deepEqual(await evaluate(R62, { document }), [ passed(R62, target, { - 1: Outcomes.IsDistinguishable, - 2: Outcomes.IsDistinguishableWhenVisited, + 1: Outcomes.IsDistinguishable(styles, styles, styles), }), ]); }); @@ -248,7 +285,7 @@ test(`evaluate() passes an applicable <a> element that removes the default text decoration and instead applies a bottom border`, async (t) => { const target = <a href="#">Link</a>; - const document = Document.of( + const document = h.document( [<p>Hello {target}</p>], [ h.sheet([ @@ -260,10 +297,31 @@ test(`evaluate() passes an applicable <a> element that removes the default text ] ); + const styles = Ok.of( + ComputedStyles.of([ + ["border-width", "0px 0px 1px"], + ["border-style", "none none solid"], + ["border-color", "currentcolor currentcolor rgb(0% 0% 0%)"], + ["color", "rgb(0% 0% 93.33333%)"], + ["outline", "0px"], + ]) + ); + t.deepEqual(await evaluate(R62, { document }), [ passed(R62, target, { - 1: Outcomes.IsDistinguishable, - 2: Outcomes.IsDistinguishableWhenVisited, + 1: Outcomes.IsDistinguishable( + styles, + styles, + Ok.of( + ComputedStyles.of([ + ["color", "rgb(0% 0% 93.33333%)"], + ["outline", "auto"], + ["border-width", "0px 0px 1px"], + ["border-style", "none none solid"], + ["border-color", "currentcolor currentcolor rgb(0% 0% 0%)"], + ]) + ) + ), }), ]); }); @@ -272,7 +330,7 @@ test(`evaluate() fails an <a> element that has no distinguishing features and has a transparent bottom border`, async (t) => { const target = <a href="#">Link</a>; - const document = Document.of( + const document = h.document( [<p>Hello {target}</p>], [ h.sheet([ @@ -284,10 +342,31 @@ test(`evaluate() fails an <a> element that has no distinguishing features and ] ); + const styles = Err.of( + ComputedStyles.of([ + ["color", "rgb(0% 0% 93.33333%)"], + ["border-width", "0px 0px 1px"], + ["border-style", "none none solid"], + ["border-color", "currentcolor currentcolor rgb(0% 0% 0% / 0%)"], + ["outline", "0px"], + ]) + ); + t.deepEqual(await evaluate(R62, { document }), [ failed(R62, target, { - 1: Outcomes.IsNotDistinguishable, - 2: Outcomes.IsNotDistinguishableWhenVisited, + 1: Outcomes.IsNotDistinguishable( + styles, + styles, + Ok.of( + ComputedStyles.of([ + ["color", "rgb(0% 0% 93.33333%)"], + ["border-width", "0px 0px 1px"], + ["border-style", "none none solid"], + ["border-color", "currentcolor currentcolor rgb(0% 0% 0% / 0%)"], + ["outline", "auto"], + ]) + ) + ), }), ]); }); @@ -296,7 +375,7 @@ test(`evaluate() fails an <a> element that has no distinguishing features and has a 0px bottom border`, async (t) => { const target = <a href="#">Link</a>; - const document = Document.of( + const document = h.document( [<p>Hello {target}</p>], [ h.sheet([ @@ -308,10 +387,31 @@ test(`evaluate() fails an <a> element that has no distinguishing features and ] ); + const styles = Err.of( + ComputedStyles.of([ + ["color", "rgb(0% 0% 93.33333%)"], + ["border-width", "0px"], + ["border-style", "none none solid"], + ["border-color", "currentcolor currentcolor rgb(0% 0% 0%)"], + ["outline", "0px"], + ]) + ); + t.deepEqual(await evaluate(R62, { document }), [ failed(R62, target, { - 1: Outcomes.IsNotDistinguishable, - 2: Outcomes.IsNotDistinguishableWhenVisited, + 1: Outcomes.IsNotDistinguishable( + styles, + styles, + Ok.of( + ComputedStyles.of([ + ["color", "rgb(0% 0% 93.33333%)"], + ["outline", "auto"], + ["border-width", "0px"], + ["border-style", "none none solid"], + ["border-color", "currentcolor currentcolor rgb(0% 0% 0%)"], + ]) + ) + ), }), ]); }); @@ -320,7 +420,7 @@ test(`evaluate() passes an applicable <a> element that removes the default text decoration and instead applies a background color`, async (t) => { const target = <a href="#">Link</a>; - const document = Document.of( + const document = h.document( [<p>Hello {target}</p>], [ h.sheet([ @@ -332,10 +432,29 @@ test(`evaluate() passes an applicable <a> element that removes the default text ] ); + const styles = Ok.of( + ComputedStyles.of([ + ["border-width", "0px"], + ["color", "rgb(0% 0% 93.33333%)"], + ["background-color", "rgb(100% 0% 0%)"], + ["outline", "0px"], + ]) + ); + t.deepEqual(await evaluate(R62, { document }), [ passed(R62, target, { - 1: Outcomes.IsDistinguishable, - 2: Outcomes.IsDistinguishableWhenVisited, + 1: Outcomes.IsDistinguishable( + styles, + styles, + Ok.of( + ComputedStyles.of([ + ["border-width", "0px"], + ["color", "rgb(0% 0% 93.33333%)"], + ["background-color", "rgb(100% 0% 0%)"], + ["outline", "auto"], + ]) + ) + ), }), ]); }); @@ -344,7 +463,7 @@ test(`evaluate() fails an <a> element that has no distinguishing features but is part of a paragraph with a background color`, async (t) => { const target = <a href="#">Link</a>; - const document = Document.of( + const document = h.document( [<p>Hello {target}</p>], [ h.sheet([ @@ -361,8 +480,17 @@ test(`evaluate() fails an <a> element that has no distinguishing features but is t.deepEqual(await evaluate(R62, { document }), [ failed(R62, target, { - 1: Outcomes.IsNotDistinguishable, - 2: Outcomes.IsNotDistinguishableWhenVisited, + 1: Outcomes.IsNotDistinguishable( + noStyle, + noStyle, + Ok.of( + ComputedStyles.of([ + ["border-width", "0px"], + ["color", "rgb(0% 0% 93.33333%)"], + ["outline", "auto"], + ]) + ) + ), }), ]); }); @@ -371,7 +499,7 @@ test(`evaluate() fails an <a> element that has no distinguishing features and has a background color equal to that of the paragraph`, async (t) => { const target = <a href="#">Link</a>; - const document = Document.of( + const document = h.document( [<p>Hello {target}</p>], [ h.sheet([ @@ -387,10 +515,29 @@ test(`evaluate() fails an <a> element that has no distinguishing features and ] ); + const styles = Err.of( + ComputedStyles.of([ + ["border-width", "0px"], + ["color", "rgb(0% 0% 93.33333%)"], + ["background-color", "rgb(100% 0% 0%)"], + ["outline", "0px"], + ]) + ); + t.deepEqual(await evaluate(R62, { document }), [ failed(R62, target, { - 1: Outcomes.IsNotDistinguishable, - 2: Outcomes.IsNotDistinguishableWhenVisited, + 1: Outcomes.IsNotDistinguishable( + styles, + styles, + Ok.of( + ComputedStyles.of([ + ["border-width", "0px"], + ["color", "rgb(0% 0% 93.33333%)"], + ["background-color", "rgb(100% 0% 0%)"], + ["outline", "auto"], + ]) + ) + ), }), ]); }); @@ -402,7 +549,7 @@ test(`evaluate() is inapplicable to an <a> element with no visible text content` </a> ); - const document = Document.of([<p>Hello {target}</p>]); + const document = h.document([<p>Hello {target}</p>]); t.deepEqual(await evaluate(R62, { document }), [inapplicable(R62)]); }); @@ -411,7 +558,7 @@ test(`evaluate() is inapplicable to an <a> element with a <p> parent element no non-link text content`, async (t) => { const target = <a href="#">Link</a>; - const document = Document.of([<p>{target}</p>]); + const document = h.document([<p>{target}</p>]); t.deepEqual(await evaluate(R62, { document }), [inapplicable(R62)]); }); @@ -420,7 +567,7 @@ test(`evaluate() is inapplicable to an <a> element with a <p> parent element no visible non-link text content`, async (t) => { const target = <a href="#">Link</a>; - const document = Document.of([ + const document = h.document([ <p> <span hidden>Hello</span> {target} </p>, @@ -433,7 +580,7 @@ test(`evaluate() passes an <a> element with a <div role="paragraph"> parent elem with non-link text content in a <span> child element`, async (t) => { const target = <a href="#">Link</a>; - const document = Document.of([ + const document = h.document([ <div role="paragraph"> <span>Hello</span> {target} </div>, @@ -441,8 +588,7 @@ test(`evaluate() passes an <a> element with a <div role="paragraph"> parent elem t.deepEqual(await evaluate(R62, { document }), [ passed(R62, target, { - 1: Outcomes.IsDistinguishable, - 2: Outcomes.IsDistinguishableWhenVisited, + 1: Outcomes.IsDistinguishable(defaultStyle, defaultStyle, focusStyle), }), ]); }); @@ -451,7 +597,7 @@ test(`evaluate() is inapplicable to an <a> element with a <p> parent element whose role has been changed`, async (t) => { const target = <a href="#">Link</a>; - const document = Document.of([ + const document = h.document([ <p role="generic"> <span>Hello</span> {target} </p>, @@ -459,3 +605,49 @@ test(`evaluate() is inapplicable to an <a> element with a <p> parent element t.deepEqual(await evaluate(R62, { document }), [inapplicable(R62)]); }); + +test(`evaluate() passes a link whose bolder than surrounding text`, async (t) => { + const target = <a href="#">Link</a>; + + const document = h.document( + [ + <p> + <span>Hello</span> {target} + </p>, + ], + [ + h.sheet([ + h.rule.style("a", { + textDecoration: "none", + fontWeight: "bold", + }), + ]), + ] + ); + + const styles = Ok.of( + ComputedStyles.of([ + ["border-width", "0px"], + ["color", "rgb(0% 0% 93.33333%)"], + ["font-weight", "700"], + ["outline", "0px"], + ]) + ); + + t.deepEqual(await evaluate(R62, { document }), [ + passed(R62, target, { + 1: Outcomes.IsDistinguishable( + styles, + styles, + Ok.of( + ComputedStyles.of([ + ["border-width", "0px"], + ["color", "rgb(0% 0% 93.33333%)"], + ["font-weight", "700"], + ["outline", "auto"], + ]) + ) + ), + }), + ]); +}); diff --git a/packages/alfa-rules/test/sia-r63/rule.spec.tsx b/packages/alfa-rules/test/sia-r63/rule.spec.tsx index bfc782bdbb..a9569a0c30 100644 --- a/packages/alfa-rules/test/sia-r63/rule.spec.tsx +++ b/packages/alfa-rules/test/sia-r63/rule.spec.tsx @@ -1,8 +1,6 @@ -import { h } from "@siteimprove/alfa-dom/h"; +import { h } from "@siteimprove/alfa-dom"; import { test } from "@siteimprove/alfa-test"; -import { Document } from "@siteimprove/alfa-dom"; - import R63, { Outcomes } from "../../src/sia-r63/rule"; import { evaluate } from "../common/evaluate"; @@ -11,7 +9,7 @@ import { passed, failed, inapplicable } from "../common/outcome"; test("evaluate() passes an object with a non-empty name", async (t) => { const target = <object aria-label="Some object" />; - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R63, { document }), [ passed(R63, target, { @@ -23,7 +21,7 @@ test("evaluate() passes an object with a non-empty name", async (t) => { test("evaluate() fails an object with an empty name", async (t) => { const target = <object aria-label="" />; - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R63, { document }), [ failed(R63, target, { @@ -37,7 +35,7 @@ test("evaluate() fails an object with no name", async (t) => { // accessibility tree. const target = <object>{h.document(["Some nested document"])}</object>; - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R63, { document }), [ failed(R63, target, { @@ -49,7 +47,7 @@ test("evaluate() fails an object with no name", async (t) => { test("evaluate() is inapplicable if there is no object", async (t) => { const target = <img src="foo.jpg" />; - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R63, { document }), [inapplicable(R63)]); }); @@ -57,7 +55,7 @@ test("evaluate() is inapplicable if there is no object", async (t) => { test("evaluate() is inapplicable on empty object", async (t) => { const target = <object title="Some object" />; - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R63, { document }), [inapplicable(R63)]); }); diff --git a/packages/alfa-rules/test/sia-r64/rule.spec.tsx b/packages/alfa-rules/test/sia-r64/rule.spec.tsx index 2bd6641da1..559d4a36e7 100644 --- a/packages/alfa-rules/test/sia-r64/rule.spec.tsx +++ b/packages/alfa-rules/test/sia-r64/rule.spec.tsx @@ -1,7 +1,6 @@ +import { h } from "@siteimprove/alfa-dom"; import { test } from "@siteimprove/alfa-test"; -import { Document } from "@siteimprove/alfa-dom"; - import R64, { Outcomes } from "../../src/sia-r64/rule"; import { evaluate } from "../common/evaluate"; @@ -10,7 +9,7 @@ import { passed, failed, inapplicable } from "../common/outcome"; test("evaluate() passes a heading that has an accessible name", async (t) => { const target = <h1>Hello world</h1>; - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R64, { document }), [ passed(R64, target, { @@ -22,7 +21,7 @@ test("evaluate() passes a heading that has an accessible name", async (t) => { test("evaluate() fails a heading that has no accessible name", async (t) => { const target = <h1 />; - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R64, { document }), [ failed(R64, target, { @@ -32,7 +31,7 @@ test("evaluate() fails a heading that has no accessible name", async (t) => { }); test("evaluate() is not applicable when a document has no headings", async (t) => { - const document = Document.of([<p>Hello world</p>]); + const document = h.document([<p>Hello world</p>]); t.deepEqual(await evaluate(R64, { document }), [inapplicable(R64)]); }); @@ -44,7 +43,7 @@ test("evaluate() fails a heading whose content is aria-hidden", async (t) => { </h1> ); - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R64, { document }), [ failed(R64, target, { diff --git a/packages/alfa-rules/test/sia-r65/rule.spec.tsx b/packages/alfa-rules/test/sia-r65/rule.spec.tsx index 578f13dc05..e25931606b 100644 --- a/packages/alfa-rules/test/sia-r65/rule.spec.tsx +++ b/packages/alfa-rules/test/sia-r65/rule.spec.tsx @@ -1,8 +1,6 @@ -import { h } from "@siteimprove/alfa-dom/h"; +import { h } from "@siteimprove/alfa-dom"; import { test } from "@siteimprove/alfa-test"; -import { Document } from "@siteimprove/alfa-dom"; - import R65, { Outcomes } from "../../src/sia-r65/rule"; import { evaluate } from "../common/evaluate"; @@ -12,7 +10,7 @@ import { oracle } from "../common/oracle"; test(`evaluate() passes an <a> element that uses the default focus outline`, async (t) => { const target = <a href="#">Link</a>; - const document = Document.of([target, <button />]); + const document = h.document([target, <button />]); t.deepEqual(await evaluate(R65, { document }), [ passed(R65, target, { @@ -27,7 +25,7 @@ test(`evaluate() passes an <a> element that uses the default focus outline`, asy test(`evaluate() passes an <a> element that uses a non-default focus outline`, async (t) => { const target = <a href="#">Link</a>; - const document = Document.of( + const document = h.document( [target, <button />], [ h.sheet([ @@ -52,7 +50,7 @@ test(`evaluate() fails an <a> element that removes the default focus outline and is determined to have no other focus indicator`, async (t) => { const target = <a href="#">Link</a>; - const document = Document.of( + const document = h.document( [target, <button />], [ h.sheet([ @@ -86,7 +84,7 @@ test(`evaluate() passes an <a> element that removes the default focus outline and is determined to have some other focus indicator`, async (t) => { const target = <a href="#">Link</a>; - const document = Document.of( + const document = h.document( [target, <button />], [ h.sheet([ @@ -120,7 +118,7 @@ test(`evaluate() passes an <a> element that removes the default focus outline and applies an underline on focus`, async (t) => { const target = <a href="#">Link</a>; - const document = Document.of( + const document = h.document( [target, <button />], [ h.sheet([ @@ -150,7 +148,7 @@ test(`evaluate() passes an <a> element that removes the default focus outline and whose parent applies an outline when focus is within the parent`, async (t) => { const target = <a href="#">Link</a>; - const document = Document.of( + const document = h.document( [<div>{target}</div>, <button />], [ h.sheet([ @@ -183,7 +181,7 @@ test(`evaluate() passes an <a> element that removes the default focus outline </a> ); - const document = Document.of( + const document = h.document( [target, <button />], [ h.sheet([ @@ -212,7 +210,7 @@ test(`evaluate() passes an <a> element that removes the default focus outline and applies an outline only when focus should be visible`, async (t) => { const target = <a href="#">Link</a>; - const document = Document.of( + const document = h.document( [target, <button />], [ h.sheet([ @@ -242,7 +240,7 @@ test(`evaluate() fails an <a> element that removes the default focus outline indicator`, async (t) => { const target = <a href="#">Link</a>; - const document = Document.of( + const document = h.document( [target, <button />], [ h.sheet([ @@ -276,7 +274,7 @@ test(`evaluate() passes an <a> element that removes the default focus outline and applies a different color on focus`, async (t) => { const target = <a href="#">Link</a>; - const document = Document.of( + const document = h.document( [target, <button />], [ h.sheet([ @@ -308,7 +306,7 @@ test(`evaluate() passes an <a> element that removes the default focus outline and applies a different background color on focus`, async (t) => { const target = <a href="#">Link</a>; - const document = Document.of( + const document = h.document( [target, <button />], [ h.sheet([ @@ -340,7 +338,7 @@ test(`evaluate() passes an <a> element that removes the default focus outline and applies a box shadow on focus`, async (t) => { const target = <a href="#">Link</a>; - const document = Document.of( + const document = h.document( [target, <button />], [ h.sheet([ @@ -365,3 +363,58 @@ test(`evaluate() passes an <a> element that removes the default focus outline }), ]); }); + +test(`evaluate() passes an <a> element that removes the default focus outline + and applies a border on focus`, async (t) => { + const target = <a href="#">Link</a>; + + const document = h.document( + [target, <button />], + [ + h.sheet([ + h.rule.style("a:focus", { + outline: "none", + border: "solid 1px", + }), + ]), + ] + ); + + t.deepEqual(await evaluate(R65, { document }), [ + passed(R65, target, { + 1: Outcomes.HasFocusIndicator, + }), + passed(R65, <button />, { + 1: Outcomes.HasFocusIndicator, + }), + ]); +}); + +test(`evaluate() passes an <a> element that removes the default focus outline + and changes border color on focus`, async (t) => { + const target = <a href="#">Link</a>; + + const document = h.document( + [target, <button />], + [ + h.sheet([ + h.rule.style("a", { + border: "solid 1px black", + }), + h.rule.style("a:focus", { + outline: "none", + border: "solid 1px red", + }), + ]), + ] + ); + + t.deepEqual(await evaluate(R65, { document }), [ + passed(R65, target, { + 1: Outcomes.HasFocusIndicator, + }), + passed(R65, <button />, { + 1: Outcomes.HasFocusIndicator, + }), + ]); +}); diff --git a/packages/alfa-rules/test/sia-r66/rule.spec.tsx b/packages/alfa-rules/test/sia-r66/rule.spec.tsx index 49b71f854f..67268fcd1c 100644 --- a/packages/alfa-rules/test/sia-r66/rule.spec.tsx +++ b/packages/alfa-rules/test/sia-r66/rule.spec.tsx @@ -1,7 +1,7 @@ +import { h } from "@siteimprove/alfa-dom"; import { test } from "@siteimprove/alfa-test"; import { RGB, Percentage } from "@siteimprove/alfa-css"; -import { Document, Text } from "@siteimprove/alfa-dom"; import R66 from "../../src/sia-r66/rule"; import { Contrast as Diagnostic } from "../../src/common/diagnostic/contrast"; @@ -21,9 +21,9 @@ const rgb = (r: number, g: number, b: number, a: number = 1) => ); test("evaluate() passes a text node that has sufficient contrast", async (t) => { - const target = Text.of("Hello world"); + const target = h.text("Hello world"); - const document = Document.of([ + const document = h.document([ <html style={{ backgroundColor: "black", color: "white" }}>{target}</html>, ]); @@ -37,10 +37,10 @@ test("evaluate() passes a text node that has sufficient contrast", async (t) => }); test("evaluate() correctly handles semi-transparent backgrounds", async (t) => { - const target1 = Text.of("Sufficient contrast"); - const target2 = Text.of("Insufficient contrast"); + const target1 = h.text("Sufficient contrast"); + const target2 = h.text("Insufficient contrast"); - const document = Document.of([ + const document = h.document([ <html style={{ backgroundColor: "black", color: "white" }}> <div style={{ backgroundColor: "rgb(100%, 100%, 100%, 15%)" }}> {target1} @@ -66,10 +66,10 @@ test("evaluate() correctly handles semi-transparent backgrounds", async (t) => { }); test("evaluate() correctly handles semi-transparent foregrounds", async (t) => { - const target1 = Text.of("Sufficient contrast"); - const target2 = Text.of("Insufficient contrast"); + const target1 = h.text("Sufficient contrast"); + const target2 = h.text("Insufficient contrast"); - const document = Document.of([ + const document = h.document([ <html style={{ backgroundColor: "black" }}> <div style={{ color: "rgb(100%, 100%, 100%, 85%)" }}>{target1}</div> <div style={{ color: "rgb(100%, 100%, 100%, 50%)" }}>{target2}</div> @@ -91,9 +91,9 @@ test("evaluate() correctly handles semi-transparent foregrounds", async (t) => { }); test("evaluate() passes an 18pt text node with sufficient contrast", async (t) => { - const target = Text.of("Hello world"); + const target = h.text("Hello world"); - const document = Document.of([ + const document = h.document([ <html style={{ backgroundColor: "black", color: "#808080", fontSize: "18pt" }} > @@ -115,9 +115,9 @@ test("evaluate() passes an 18pt text node with sufficient contrast", async (t) = }); test("evaluate() passes an 14pt, bold text node with sufficient contrast", async (t) => { - const target = Text.of("Hello world"); + const target = h.text("Hello world"); - const document = Document.of([ + const document = h.document([ <html style={{ backgroundColor: "black", @@ -144,9 +144,9 @@ test("evaluate() passes an 14pt, bold text node with sufficient contrast", async }); test("evaluate() passes a text node using the user agent default styles", async (t) => { - const target = Text.of("Hello world"); + const target = h.text("Hello world"); - const document = Document.of([<html>{target}</html>]); + const document = h.document([<html>{target}</html>]); t.deepEqual(await evaluate(R66, { document }), [ passed(R66, target, { @@ -158,9 +158,9 @@ test("evaluate() passes a text node using the user agent default styles", async }); test("evaluate() correctly resolves the `currentcolor` keyword", async (t) => { - const target = Text.of("Hello world"); + const target = h.text("Hello world"); - const document = Document.of([ + const document = h.document([ <html style={{ backgroundColor: "currentcolor", color: "white" }}> {target} </html>, @@ -176,9 +176,9 @@ test("evaluate() correctly resolves the `currentcolor` keyword", async (t) => { }); test("evaluate() correctly resolves the `currentcolor` keyword to the user agent default", async (t) => { - const target = Text.of("Hello world"); + const target = h.text("Hello world"); - const document = Document.of([ + const document = h.document([ <html style={{ backgroundColor: "currentcolor" }}>{target}</html>, ]); @@ -192,9 +192,9 @@ test("evaluate() correctly resolves the `currentcolor` keyword to the user agent }); test("evaluate() correctly handles circular `currentcolor` references", async (t) => { - const target = Text.of("Hello world"); + const target = h.text("Hello world"); - const document = Document.of([ + const document = h.document([ <html style={{ color: "currentcolor" }}>{target}</html>, ]); @@ -202,7 +202,7 @@ test("evaluate() correctly handles circular `currentcolor` references", async (t }); test("evaluate() is inapplicable to text nodes in widgets", async (t) => { - const document = Document.of([ + const document = h.document([ <html> <button>Hello world</button> </html>, @@ -212,7 +212,7 @@ test("evaluate() is inapplicable to text nodes in widgets", async (t) => { }); test("evaluate() is inapplicable to text nodes in disabled groups", async (t) => { - const document = Document.of([ + const document = h.document([ <html> <fieldset disabled> <button>Hello world</button> @@ -224,9 +224,9 @@ test("evaluate() is inapplicable to text nodes in disabled groups", async (t) => }); test("evaluate() passes when a background color with sufficient contrast is input", async (t) => { - const target = Text.of("Hello world"); + const target = h.text("Hello world"); - const document = Document.of([ + const document = h.document([ <html style={{ color: "#000", backgroundImage: "url('foo.png')" }}> {target} </html>, @@ -251,9 +251,9 @@ test("evaluate() passes when a background color with sufficient contrast is inpu }); test("evaluate() fails when a background color with insufficient contrast is input", async (t) => { - const target = Text.of("Hello world"); + const target = h.text("Hello world"); - const document = Document.of([ + const document = h.document([ <html style={{ color: "#000", backgroundImage: "url('foo.png')" }}> {target} </html>, @@ -278,9 +278,9 @@ test("evaluate() fails when a background color with insufficient contrast is inp }); test("evaluate() passes when a linear gradient has sufficient contrast in the best case", async (t) => { - const target = Text.of("Hello world"); + const target = h.text("Hello world"); - const document = Document.of([ + const document = h.document([ <html style={{ color: "#000", @@ -303,9 +303,9 @@ test("evaluate() passes when a linear gradient has sufficient contrast in the be }); test("evaluate() fails when a linear gradient has insufficient contrast in the best case", async (t) => { - const target = Text.of("Hello world"); + const target = h.text("Hello world"); - const document = Document.of([ + const document = h.document([ <html style={{ color: "#000", @@ -329,9 +329,9 @@ test("evaluate() fails when a linear gradient has insufficient contrast in the b test(`evaluate() correctly merges semi-transparent background layers against a white backdrop`, async (t) => { - const target = Text.of("Hello world"); + const target = h.text("Hello world"); - const document = Document.of([ + const document = h.document([ <div style={{ color: "#fff", diff --git a/packages/alfa-rules/test/sia-r67/rule.spec.tsx b/packages/alfa-rules/test/sia-r67/rule.spec.tsx index db0ea7c2d9..ed820f767c 100644 --- a/packages/alfa-rules/test/sia-r67/rule.spec.tsx +++ b/packages/alfa-rules/test/sia-r67/rule.spec.tsx @@ -1,6 +1,7 @@ +import { h } from "@siteimprove/alfa-dom"; import { test } from "@siteimprove/alfa-test"; -import { Document, Namespace } from "@siteimprove/alfa-dom"; +import { Namespace } from "@siteimprove/alfa-dom"; import R67, { Outcomes } from "../../src/sia-r67/rule"; @@ -11,7 +12,7 @@ test(`evaluate() passes an <img> element that is marked as decorative and not included in the accessibility tree`, async (t) => { const target = <img src="foo.jpg" alt="" />; - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R67, { document }), [ passed(R67, target, { @@ -24,7 +25,7 @@ test(`evaluate() passes an <svg> element that is marked as decorative and not included in the accessibility tree`, async (t) => { const target = <svg xmlns={Namespace.SVG} role="presentation" />; - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R67, { document }), [ passed(R67, target, { @@ -37,7 +38,7 @@ test(`evaluate() fails an <img> element that is marked as decorative but is still included in the accessiblity tree`, async (t) => { const target = <img src="foo.jpg" alt="" aria-label="Foo" />; - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R67, { document }), [ failed(R67, target, { @@ -52,7 +53,7 @@ test(`evaluate() fails an <svg> element that is marked as decorative but is <svg xmlns={Namespace.SVG} role="presentation" aria-label="Foo" /> ); - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R67, { document }), [ failed(R67, target, { @@ -62,20 +63,20 @@ test(`evaluate() fails an <svg> element that is marked as decorative but is }); test("evaluate() is inapplicabale to an <img> that is not marked as decorative", async (t) => { - const document = Document.of([<img src="foo.jpg" />]); + const document = h.document([<img src="foo.jpg" />]); t.deepEqual(await evaluate(R67, { document }), [inapplicable(R67)]); }); test("evaluate() is inapplicabale to an <img> that is not marked as decorative", async (t) => { - const document = Document.of([<img src="foo.jpg" />]); + const document = h.document([<img src="foo.jpg" />]); t.deepEqual(await evaluate(R67, { document }), [inapplicable(R67)]); }); test(`evaluate() is inapplicable to an <span> element that is marked as decorative`, async (t) => { - const document = Document.of([<span role="presentation" />]); + const document = h.document([<span role="presentation" />]); t.deepEqual(await evaluate(R67, { document }), [inapplicable(R67)]); }); diff --git a/packages/alfa-rules/test/sia-r68/rule.spec.tsx b/packages/alfa-rules/test/sia-r68/rule.spec.tsx index 434c60a08d..39fd4859f4 100644 --- a/packages/alfa-rules/test/sia-r68/rule.spec.tsx +++ b/packages/alfa-rules/test/sia-r68/rule.spec.tsx @@ -1,7 +1,6 @@ +import { h } from "@siteimprove/alfa-dom"; import { test } from "@siteimprove/alfa-test"; -import { Document } from "@siteimprove/alfa-dom"; - import R68, { Outcomes } from "../../src/sia-r68/rule"; import { evaluate } from "../common/evaluate"; @@ -15,7 +14,7 @@ test("evaluate() passes a list with two list items", async (t) => { </div> ); - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R68, { document }), [ passed(R68, target, { @@ -46,7 +45,7 @@ test("evaluate() passes a table with a row and a row group", async (t) => { </div> ); - const document = Document.of([target4]); + const document = h.document([target4]); t.deepEqual(await evaluate(R68, { document }), [ passed(R68, target4, { @@ -78,7 +77,7 @@ test("evaluate() passes a table with a caption and a row", async (t) => { </div> ); - const document = Document.of([target2]); + const document = h.document([target2]); t.deepEqual(await evaluate(R68, { document }), [ passed(R68, target2, { @@ -111,7 +110,7 @@ test("evaluate() passes a table with a caption and two rows", async (t) => { </div> ); - const document = Document.of([target3]); + const document = h.document([target3]); t.deepEqual(await evaluate(R68, { document }), [ passed(R68, target3, { @@ -134,7 +133,7 @@ test("evaluate() passes a radiogroup with a radio and a label", async (t) => { </div> ); - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R68, { document }), [ passed(R68, target, { @@ -151,7 +150,7 @@ test("evaluate() ignores non-element children when determining ownership", async </div> ); - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R68, { document }), [ passed(R68, target, { @@ -167,7 +166,7 @@ test("evaluate() fails a list with only a non-list item", async (t) => { </div> ); - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R68, { document }), [ failed(R68, target, { @@ -183,6 +182,6 @@ test("evaluate() is inapplicable to aria-busy elements", async (t) => { </ul> ); - const document = Document.of([menu]); + const document = h.document([menu]); t.deepEqual(await evaluate(R68, { document }), [inapplicable(R68)]); }); diff --git a/packages/alfa-rules/test/sia-r69/rule.spec.tsx b/packages/alfa-rules/test/sia-r69/rule.spec.tsx index 0e9354983c..6799efc9b6 100644 --- a/packages/alfa-rules/test/sia-r69/rule.spec.tsx +++ b/packages/alfa-rules/test/sia-r69/rule.spec.tsx @@ -1,4 +1,4 @@ -import { h } from "@siteimprove/alfa-dom/h"; +import { h } from "@siteimprove/alfa-dom"; import { test } from "@siteimprove/alfa-test"; import { RGB, Percentage } from "@siteimprove/alfa-css"; @@ -417,3 +417,24 @@ test(`evaluate() correctly merges semi-transparent background layers against a }), ]); }); + +test(`evaluate() cannot tell when a background has a fixed size`, async (t) => { + const target = h.text("Hello World"); + + const div = ( + <div + style={{ + backgroundImage: + "linear-gradient(to right,rgb(0, 0, 0) 0%, rgb(0, 0, 0) 100%)", + backgroundRepeat: "repeat-x", + backgroundPosition: "0px 100%", + backgroundSize: "100% 2px", + }} + > + {target} + </div> + ); + const document = h.document([div]); + + t.deepEqual(await evaluate(R69, { document }), [cantTell(R69, target)]); +}); diff --git a/packages/alfa-rules/test/sia-r7/rule.spec.tsx b/packages/alfa-rules/test/sia-r7/rule.spec.tsx index 44cae26faf..e925f5815a 100644 --- a/packages/alfa-rules/test/sia-r7/rule.spec.tsx +++ b/packages/alfa-rules/test/sia-r7/rule.spec.tsx @@ -1,7 +1,6 @@ +import { h } from "@siteimprove/alfa-dom"; import { test } from "@siteimprove/alfa-test"; -import { Document } from "@siteimprove/alfa-dom"; - import R7, { Outcomes } from "../../src/sia-r7/rule"; import { evaluate } from "../common/evaluate"; @@ -11,7 +10,7 @@ test("evaluate() passes an element with a lang attribute within <body> with a va const element = <span lang="en">Hello World</span>; const target = element.attribute("lang").get(); - const document = Document.of([<body>{element}</body>]); + const document = h.document([<body>{element}</body>]); t.deepEqual(await evaluate(R7, { document }), [ passed(R7, target, { @@ -28,7 +27,7 @@ test("evaluate() passes an element with a lang attribute within <body> that is n ); const target = element.attribute("lang").get(); - const document = Document.of([<body>{element}</body>]); + const document = h.document([<body>{element}</body>]); t.deepEqual(await evaluate(R7, { document }), [ passed(R7, target, { @@ -45,7 +44,7 @@ test("evaluate() passes an element with a lang attribute within <body> that is n ); const target = element.attribute("lang").get(); - const document = Document.of([<body>{element}</body>]); + const document = h.document([<body>{element}</body>]); t.deepEqual(await evaluate(R7, { document }), [ passed(R7, target, { @@ -62,7 +61,7 @@ test("evaluate() passes an element with a lang attribute within <body> when the ); const target = element.attribute("lang").get(); - const document = Document.of([ + const document = h.document([ <body> <div lang="invalid">{element}</div> </body>, @@ -79,7 +78,7 @@ test("evaluate() fails an element with a lang attribute within <body> with an in const element = <span lang="invalid">Hello World</span>; const target = element.attribute("lang").get()!; - const document = Document.of([<body>{element}</body>]); + const document = h.document([<body>{element}</body>]); t.deepEqual(await evaluate(R7, { document }), [ failed(R7, target, { @@ -95,7 +94,7 @@ test("evaluate() correctly handles nested elements with valid/invalid lang attri const target1 = element1.attribute("lang").get(); const target2 = element2.attribute("lang").get(); - const document = Document.of([<body>{element2}</body>]); + const document = h.document([<body>{element2}</body>]); t.deepEqual(await evaluate(R7, { document }), [ failed(R7, target2, { @@ -110,7 +109,7 @@ test("evaluate() correctly handles nested elements with valid/invalid lang attri test("evaluate() is inapplicable for an element that is not visible or included in the accessibility tree", async (t) => { const element = <span lang="invalid" hidden />; - const document = Document.of([<body>{element}</body>]); + const document = h.document([<body>{element}</body>]); t.deepEqual(await evaluate(R7, { document }), [inapplicable(R7)]); }); @@ -118,7 +117,7 @@ test("evaluate() is inapplicable for an element that is not visible or included test("evaluate() is inapplicable for an element with empty lang attribute", async (t) => { const element = <span lang="" />; - const document = Document.of([<body>{element}</body>]); + const document = h.document([<body>{element}</body>]); t.deepEqual(await evaluate(R7, { document }), [inapplicable(R7)]); }); @@ -126,7 +125,7 @@ test("evaluate() is inapplicable for an element with empty lang attribute", asyn test("evaluate() is inapplicable for an element with only whitespace lang attribute", async (t) => { const element = <span lang=" " />; - const document = Document.of([<body>{element}</body>]); + const document = h.document([<body>{element}</body>]); t.deepEqual(await evaluate(R7, { document }), [inapplicable(R7)]); }); diff --git a/packages/alfa-rules/test/sia-r71/rule.spec.tsx b/packages/alfa-rules/test/sia-r71/rule.spec.tsx index 86c9cd6619..917f138d7f 100644 --- a/packages/alfa-rules/test/sia-r71/rule.spec.tsx +++ b/packages/alfa-rules/test/sia-r71/rule.spec.tsx @@ -1,7 +1,6 @@ +import { h } from "@siteimprove/alfa-dom"; import { test } from "@siteimprove/alfa-test"; -import { Document } from "@siteimprove/alfa-dom"; - import R71, { Outcomes } from "../../src/sia-r71/rule"; import { evaluate } from "../common/evaluate"; @@ -10,7 +9,7 @@ import { passed, failed, inapplicable } from "../common/outcome"; test("evaluate() passes a paragraph whose text is not justified", async (t) => { const target = <p>Hello world</p>; - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R71, { document }), [ passed(R71, target, { @@ -22,7 +21,7 @@ test("evaluate() passes a paragraph whose text is not justified", async (t) => { test("evaluate() fails a paragraph whose text is justified", async (t) => { const target = <p style={{ textAlign: "justify" }}>Hello world</p>; - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R71, { document }), [ failed(R71, target, { @@ -34,7 +33,7 @@ test("evaluate() fails a paragraph whose text is justified", async (t) => { test("evaluate() fails a paragraph whose text is justified by inheritance", async (t) => { const target = <p>Hello world</p>; - const document = Document.of([ + const document = h.document([ <div style={{ textAlign: "justify" }}>{target}</div>, ]); @@ -52,7 +51,7 @@ test("evaluate() fails an ARIA paragraph whose text is justified", async (t) => </div> ); - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R71, { document }), [ failed(R71, target, { @@ -68,7 +67,7 @@ test("evaluate() ignores a <p> element whose role is changed", async (t) => { </p> ); - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R71, { document }), [inapplicable(R71)]); }); diff --git a/packages/alfa-rules/test/sia-r72/rule.spec.tsx b/packages/alfa-rules/test/sia-r72/rule.spec.tsx index 8101af63b8..5e5cc88dd6 100644 --- a/packages/alfa-rules/test/sia-r72/rule.spec.tsx +++ b/packages/alfa-rules/test/sia-r72/rule.spec.tsx @@ -1,7 +1,6 @@ +import { h } from "@siteimprove/alfa-dom"; import { test } from "@siteimprove/alfa-test"; -import { Document } from "@siteimprove/alfa-dom"; - import R72, { Outcomes } from "../../src/sia-r72/rule"; import { evaluate } from "../common/evaluate"; @@ -10,7 +9,7 @@ import { passed, failed, inapplicable } from "../common/outcome"; test("evaluate() passes a paragraph whose text is not uppercased", async (t) => { const target = <p>Hello world</p>; - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R72, { document }), [ passed(R72, target, { @@ -22,7 +21,7 @@ test("evaluate() passes a paragraph whose text is not uppercased", async (t) => test("evaluate() fails a paragraph whose text is uppercased", async (t) => { const target = <p style={{ textTransform: "uppercase" }}>Hello world</p>; - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R72, { document }), [ failed(R72, target, { @@ -34,7 +33,7 @@ test("evaluate() fails a paragraph whose text is uppercased", async (t) => { test("evaluate() fails a paragraph whose text is uppercased by inheritance", async (t) => { const target = <p>Hello world</p>; - const document = Document.of([ + const document = h.document([ <div style={{ textTransform: "uppercase" }}>{target}</div>, ]); @@ -52,7 +51,7 @@ test("evaluate() fails an ARIA paragraph whose text is uppercased", async (t) => </div> ); - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R72, { document }), [ failed(R72, target, { @@ -68,7 +67,7 @@ test("evaluate() ignores a <p> whose role is changed", async (t) => { </p> ); - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R72, { document }), [inapplicable(R72)]); }); diff --git a/packages/alfa-rules/test/sia-r73/rule.spec.tsx b/packages/alfa-rules/test/sia-r73/rule.spec.tsx index 472e22e12c..047c884f29 100644 --- a/packages/alfa-rules/test/sia-r73/rule.spec.tsx +++ b/packages/alfa-rules/test/sia-r73/rule.spec.tsx @@ -1,7 +1,6 @@ +import { h } from "@siteimprove/alfa-dom"; import { test } from "@siteimprove/alfa-test"; -import { Document } from "@siteimprove/alfa-dom"; - import R73, { Outcomes } from "../../src/sia-r73/rule"; import { evaluate } from "../common/evaluate"; @@ -10,7 +9,7 @@ import { passed, failed, inapplicable } from "../common/outcome"; test("evaluate() passes a paragraph whose line height is at least 1.5", async (t) => { const target = <p style={{ lineHeight: "1.5" }}>Hello world</p>; - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R73, { document }), [ passed(R73, target, { @@ -25,7 +24,7 @@ test(`evaluate() passes a paragraph whose line height is at least 1.5 times the <p style={{ fontSize: "16px", lineHeight: "24px" }}>Hello world</p> ); - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R73, { document }), [ passed(R73, target, { @@ -37,7 +36,7 @@ test(`evaluate() passes a paragraph whose line height is at least 1.5 times the test("evaluate() fails a paragraph whose line height is less than 1.5", async (t) => { const target = <p style={{ lineHeight: "1.2" }}>Hello world</p>; - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R73, { document }), [ failed(R73, target, { @@ -52,7 +51,7 @@ test(`evaluate() fails a paragraph whose line height is less than 1.5 times the <p style={{ fontSize: "16px", lineHeight: "22px" }}>Hello world</p> ); - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R73, { document }), [ failed(R73, target, { @@ -64,7 +63,7 @@ test(`evaluate() fails a paragraph whose line height is less than 1.5 times the test(`evaluate() fails a paragraph whose line height is "normal"`, async (t) => { const target = <p style={{ lineHeight: "normal" }}>Hello world</p>; - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R73, { document }), [ failed(R73, target, { @@ -76,7 +75,7 @@ test(`evaluate() fails a paragraph whose line height is "normal"`, async (t) => test("evaluate() fails a paragraph that relies on the default line height", async (t) => { const target = <p>Hello world</p>; - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R73, { document }), [ failed(R73, target, { @@ -92,7 +91,7 @@ test("evaluate() fails an ARIA paragraph whose line height is less than 1.5", as </div> ); - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R73, { document }), [ failed(R73, target, { @@ -108,7 +107,7 @@ test("evaluate() ignores a <p> element whose role is changed", async (t) => { </p> ); - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R73, { document }), [inapplicable(R73)]); }); diff --git a/packages/alfa-rules/test/sia-r74/rule.spec.tsx b/packages/alfa-rules/test/sia-r74/rule.spec.tsx index a8d073b723..3b4edd54c5 100644 --- a/packages/alfa-rules/test/sia-r74/rule.spec.tsx +++ b/packages/alfa-rules/test/sia-r74/rule.spec.tsx @@ -1,7 +1,6 @@ +import { h } from "@siteimprove/alfa-dom"; import { test } from "@siteimprove/alfa-test"; -import { Document } from "@siteimprove/alfa-dom"; - import R74, { Outcomes } from "../../src/sia-r74/rule"; import { evaluate } from "../common/evaluate"; @@ -11,7 +10,7 @@ test(`evaluate() passes a paragraph with a font size specified using a relative length`, async (t) => { const target = <p style={{ fontSize: "1em" }}>Hello world</p>; - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R74, { document }), [ passed(R74, target, { @@ -24,7 +23,7 @@ test(`evaluate() fails a paragraph with a font size specified using an absolute length`, async (t) => { const target = <p style={{ fontSize: "16px" }}>Hello world</p>; - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R74, { document }), [ failed(R74, target, { @@ -34,13 +33,13 @@ test(`evaluate() fails a paragraph with a font size specified using an absolute }); test("evaluate() is inapplicable to a paragraph that has no text", async (t) => { - const document = Document.of([<p style={{ fontSize: "16px" }} />]); + const document = h.document([<p style={{ fontSize: "16px" }} />]); t.deepEqual(await evaluate(R74, { document }), [inapplicable(R74)]); }); test("evaluate() is inapplicable to a paragraph that isn't visible", async (t) => { - const document = Document.of([ + const document = h.document([ <p style={{ fontSize: "16px" }} hidden> Hello world </p>, @@ -57,7 +56,7 @@ test(`evaluate() fails an ARIA paragraph with a font size specified using an abs </div> ); - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R74, { document }), [ failed(R74, target, { @@ -73,7 +72,7 @@ test(`evaluate() ignores <p> element whose role is changed`, async (t) => { </p> ); - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R74, { document }), [inapplicable(R74)]); }); diff --git a/packages/alfa-rules/test/sia-r75/rule.spec.tsx b/packages/alfa-rules/test/sia-r75/rule.spec.tsx index 2d2309ce69..51e0b38a0f 100644 --- a/packages/alfa-rules/test/sia-r75/rule.spec.tsx +++ b/packages/alfa-rules/test/sia-r75/rule.spec.tsx @@ -1,7 +1,6 @@ +import { h } from "@siteimprove/alfa-dom"; import { test } from "@siteimprove/alfa-test"; -import { Document } from "@siteimprove/alfa-dom"; - import R75, { Outcomes } from "../../src/sia-r75/rule"; import { evaluate } from "../common/evaluate"; @@ -10,7 +9,7 @@ import { passed, failed } from "../common/outcome"; test("evaluate() passes an element with a font size not smaller than 9 pixels", async (t) => { const target = <html style={{ fontSize: "medium" }}>Hello world</html>; - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R75, { document }), [ passed(R75, target, { @@ -22,7 +21,7 @@ test("evaluate() passes an element with a font size not smaller than 9 pixels", test("evaluate() fails an element with a font size smaller than 9 pixels", async (t) => { const target = <div style={{ fontSize: "8px" }}>Hello world</div>; - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R75, { document }), [ failed(R75, target, { @@ -36,7 +35,7 @@ test(`evaluate() fails an element with an accumulated font size smaller than 9 const target1 = <p style={{ fontSize: "smaller" }}>Hello world</p>; const target2 = <div style={{ fontSize: "10px" }}>{target1}</div>; - const document = Document.of([target2]); + const document = h.document([target2]); t.deepEqual(await evaluate(R75, { document }), [ passed(R75, target2, { diff --git a/packages/alfa-rules/test/sia-r8/rule.spec.tsx b/packages/alfa-rules/test/sia-r8/rule.spec.tsx new file mode 100644 index 0000000000..05218aeb01 --- /dev/null +++ b/packages/alfa-rules/test/sia-r8/rule.spec.tsx @@ -0,0 +1,195 @@ +import { h } from "@siteimprove/alfa-dom"; +import { test } from "@siteimprove/alfa-test"; + +import R8, { Outcomes } from "../../src/sia-r8/rule"; + +import { evaluate } from "../common/evaluate"; +import { passed, failed, inapplicable } from "../common/outcome"; + +test("evaluate() passes an input element with implicit label", async (t) => { + const target = <input />; + + const label = ( + <label> + first name + {target} + </label> + ); + + const document = h.document([label]); + + t.deepEqual(await evaluate(R8, { document }), [ + passed(R8, target, { + 1: Outcomes.HasName, + }), + ]); +}); + +test("evaluate() passes an input element with aria-label", async (t) => { + const target = <input aria-label="last name" disabled />; + + const document = h.document([target]); + + t.deepEqual(await evaluate(R8, { document }), [ + passed(R8, target, { + 1: Outcomes.HasName, + }), + ]); +}); + +test("evaluate() passes a select element with explicit label", async (t) => { + const target = ( + <select id="country"> + <option>England</option> + <option>Scotland</option> + <option>Wales</option> + <option>Northern Ireland</option> + </select> + ); + + const label = <label for="country">Country</label>; + + const document = h.document([label, target]); + + t.deepEqual(await evaluate(R8, { document }), [ + passed(R8, target, { + 1: Outcomes.HasName, + }), + ]); +}); + +test("evaluate() passes a textarea element with aria-labelledby", async (t) => { + const target = <textarea aria-labelledby="country"></textarea>; + + const label = <div id="country">Country</div>; + + const document = h.document([label, target]); + + t.deepEqual(await evaluate(R8, { document }), [ + passed(R8, target, { + 1: Outcomes.HasName, + }), + ]); +}); + +test("evaluate() passes a input element with placeholder attribute", async (t) => { + const target = <input placeholder="Your search query" />; + + const label = <button type="submit">search</button>; + + const document = h.document([label, target]); + + t.deepEqual(await evaluate(R8, { document }), [ + passed(R8, target, { + 1: Outcomes.HasName, + }), + ]); +}); + +test(`evaluate() passes a div element with explicit combobox role and an + aria-label attribute`, async (t) => { + const target = ( + <div aria-label="country" role="combobox" aria-disabled="true"> + England + </div> + ); + + const document = h.document([target]); + + t.deepEqual(await evaluate(R8, { document }), [ + passed(R8, target, { + 1: Outcomes.HasName, + }), + ]); +}); + +test("evaluate() fails a input element without accessible name", async (t) => { + const target = <input />; + + const document = h.document([target]); + + t.deepEqual(await evaluate(R8, { document }), [ + failed(R8, target, { + 1: Outcomes.HasNoName, + }), + ]); +}); + +test("evaluate() fails a input element with empty aria-label", async (t) => { + const target = <input aria-label=" " />; + + const document = h.document([target]); + + t.deepEqual(await evaluate(R8, { document }), [ + failed(R8, target, { + 1: Outcomes.HasNoName, + }), + ]); +}); + +test(`evaluate() fails a select element with aria-labelledby pointing to an + empty element`, async (t) => { + const target = ( + <select aria-labelledby="country"> + <option>England</option> + </select> + ); + + const label = <div id="country"></div>; + + const document = h.document([label, target]); + + t.deepEqual(await evaluate(R8, { document }), [ + failed(R8, target, { + 1: Outcomes.HasNoName, + }), + ]); +}); + +test("evaluate() fails a textbox with no accessible name", async (t) => { + const target = <div role="textbox"></div>; + + const label = ( + <label> + first name + {target} + </label> + ); + const document = h.document([label, target]); + + t.deepEqual(await evaluate(R8, { document }), [ + failed(R8, target, { + 1: Outcomes.HasNoName, + }), + ]); +}); + +test("evaluate() is inapplicable for an element with aria-hidden", async (t) => { + const target = <input disabled aria-hidden="true" aria-label="firstname" />; + + const document = h.document([target]); + + t.deepEqual(await evaluate(R8, { document }), [inapplicable(R8)]); +}); + +test("evaluate() is inapplicable for a disabled element", async (t) => { + const target = ( + <select role="none" disabled> + <option value="volvo">Volvo</option> + <option value="saab">Saab</option> + <option value="opel">Opel</option> + </select> + ); + + const document = h.document([target]); + + t.deepEqual(await evaluate(R8, { document }), [inapplicable(R8)]); +}); + +test("evaluate() is inapplicable for an element which is not displayed", async (t) => { + const target = <input aria-label="firstname" style={{ display: "none" }} />; + + const document = h.document([target]); + + t.deepEqual(await evaluate(R8, { document }), [inapplicable(R8)]); +}); diff --git a/packages/alfa-rules/test/sia-r80/rule.spec.tsx b/packages/alfa-rules/test/sia-r80/rule.spec.tsx index 92e69d59c9..b5b77405f8 100644 --- a/packages/alfa-rules/test/sia-r80/rule.spec.tsx +++ b/packages/alfa-rules/test/sia-r80/rule.spec.tsx @@ -1,7 +1,6 @@ +import { h } from "@siteimprove/alfa-dom"; import { test } from "@siteimprove/alfa-test"; -import { Document } from "@siteimprove/alfa-dom"; - import R80, { Outcomes } from "../../src/sia-r80/rule"; import { evaluate } from "../common/evaluate"; @@ -11,7 +10,7 @@ test(`evaluate() passes an element with a line height specified using a relative length`, async (t) => { const target = <p style={{ lineHeight: "1.5em" }}>Hello world</p>; - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R80, { document }), [ passed(R80, target, { @@ -24,7 +23,7 @@ test(`evaluate() fails an element with a line height specified using an absolute length`, async (t) => { const target = <p style={{ lineHeight: "24px" }}>Hello world</p>; - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R80, { document }), [ failed(R80, target, { @@ -34,13 +33,13 @@ test(`evaluate() fails an element with a line height specified using an absolute }); test("evaluate() is inapplicable to an element that has no text", async (t) => { - const document = Document.of([<p style={{ lineHeight: "24px" }} />]); + const document = h.document([<p style={{ lineHeight: "24px" }} />]); t.deepEqual(await evaluate(R80, { document }), [inapplicable(R80)]); }); test("evaluate() is inapplicable to an element that isn't visible", async (t) => { - const document = Document.of([ + const document = h.document([ <p style={{ lineHeight: "24px" }} hidden> Hello world </p>, @@ -53,7 +52,7 @@ test(`evaluate() fails an element with a line height specified using an absolute length`, async (t) => { const target = <p style={{ lineHeight: "24px" }}>Hello world</p>; - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R80, { document }), [ failed(R80, target, { @@ -65,7 +64,7 @@ test(`evaluate() fails an element with a line height specified using an absolute test(`evaluate() is inapplicable to non-paragraph elements`, async (t) => { const target = <div style={{ lineHeight: "24px" }}>Hello world</div>; - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R80, { document }), [inapplicable(R80)]); }); @@ -78,7 +77,7 @@ test(`evaluate() fails an ARIA paragraph with a line height specified using an </div> ); - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R80, { document }), [ failed(R80, target, { diff --git a/packages/alfa-rules/test/sia-r81/rule.spec.tsx b/packages/alfa-rules/test/sia-r81/rule.spec.tsx index fda2880e0f..17302bfb11 100644 --- a/packages/alfa-rules/test/sia-r81/rule.spec.tsx +++ b/packages/alfa-rules/test/sia-r81/rule.spec.tsx @@ -1,7 +1,6 @@ +import { h } from "@siteimprove/alfa-dom"; import { test } from "@siteimprove/alfa-test"; -import { Document } from "@siteimprove/alfa-dom"; - import R81, { Outcomes } from "../../src/sia-r81/rule"; import { Group } from "../../src/common/group"; @@ -16,7 +15,7 @@ test(`evaluate() passes two links that have the same name and reference the same resource in the same context`, async (t) => { const target = [<a href="foo.html">Foo</a>, <a href="foo.html">Foo</a>]; - const document = Document.of([ + const document = h.document([ <html> <p> {target[0]} @@ -36,7 +35,7 @@ test(`evaluate() fails two links that have the same name, but reference different resources in the same context`, async (t) => { const target = [<a href="foo.html">Foo</a>, <a href="bar.html">Foo</a>]; - const document = Document.of([ + const document = h.document([ <html> <p> {target[0]} @@ -65,7 +64,7 @@ test(`evaluate() passes two links that have the same name and reference equivalent resources in the same context`, async (t) => { const target = [<a href="foo.html">Foo</a>, <a href="bar.html">Foo</a>]; - const document = Document.of([ + const document = h.document([ <html> <p> {target[0]} @@ -92,7 +91,7 @@ test(`evaluate() passes two links that have the same name and reference test(`evaluate() is inapplicable to two links that have the same name and reference the same resource, but have different contexts`, async (t) => { - const document = Document.of([ + const document = h.document([ <html> <p> <a href="foo.html">Foo</a> @@ -116,7 +115,7 @@ test("evaluate() correctly resolves relative URLs", async (t) => { <a href="../to/foo.html">Foo</a>, ]; - const document = Document.of([ + const document = h.document([ <p> {target[0]} {target[1]} diff --git a/packages/alfa-rules/test/sia-r82/rule.spec.tsx b/packages/alfa-rules/test/sia-r82/rule.spec.tsx index c5ec1fb5fa..5ced7cda3d 100644 --- a/packages/alfa-rules/test/sia-r82/rule.spec.tsx +++ b/packages/alfa-rules/test/sia-r82/rule.spec.tsx @@ -1,7 +1,6 @@ +import { h } from "@siteimprove/alfa-dom"; import { test } from "@siteimprove/alfa-test"; -import { Document } from "@siteimprove/alfa-dom"; - import R82, { Outcomes } from "../../src/sia-r82/rule"; import { evaluate } from "../common/evaluate"; @@ -16,7 +15,7 @@ const invisibleError = <span hidden>Invisible error</span>; const ignoredError = <span aria-hidden="true">Ignored error</span>; -const document = Document.of([ +const document = h.document([ <form> <label> Input diff --git a/packages/alfa-rules/test/sia-r83/rule.spec.tsx b/packages/alfa-rules/test/sia-r83/rule.spec.tsx index fe6276a5d3..bb54181a4f 100644 --- a/packages/alfa-rules/test/sia-r83/rule.spec.tsx +++ b/packages/alfa-rules/test/sia-r83/rule.spec.tsx @@ -1,8 +1,6 @@ -import { h } from "@siteimprove/alfa-dom/h"; +import { h } from "@siteimprove/alfa-dom"; import { test } from "@siteimprove/alfa-test"; -import { Document } from "@siteimprove/alfa-dom"; - import R83, { Outcomes } from "../../src/sia-r83/rule"; import { evaluate } from "../common/evaluate"; @@ -11,7 +9,7 @@ import { passed, failed, inapplicable } from "../common/outcome"; test("evaluate() passes a text node that truncates overflow using ellipsis", async (t) => { const target = h.text("Hello world"); - const document = Document.of( + const document = h.document( [<div>{target}</div>], [ h.sheet([ @@ -35,7 +33,7 @@ test(`evaluate() passes a child text node of an element whose parent truncates overflow using ellipsis`, async (t) => { const target = h.text("Hello world"); - const document = Document.of( + const document = h.document( [ <div> <span>{target}</span> @@ -63,7 +61,7 @@ test(`evaluate() fails a text node that clips overflow by not wrapping text using the \`white-space\` property`, async (t) => { const target = h.text("Hello world"); - const document = Document.of( + const document = h.document( [<div>{target}</div>], [ h.sheet([ @@ -86,7 +84,7 @@ test(`evaluate() fails a text node that clips overflow and sets a fixed height using the px unit`, async (t) => { const target = h.text("Hello world"); - const document = Document.of( + const document = h.document( [<div>{target}</div>], [ h.sheet([ @@ -109,7 +107,7 @@ test(`evaluate() fails a text node that clips overflow and sets a fixed height using the vh unit`, async (t) => { const target = h.text("Hello world"); - const document = Document.of( + const document = h.document( [<div>{target}</div>], [ h.sheet([ @@ -129,7 +127,7 @@ test(`evaluate() fails a text node that clips overflow and sets a fixed height }); test("evaluate() is inapplicable to a text node that is not visible", async (t) => { - const document = Document.of( + const document = h.document( [<div hidden>Hello world</div>], [ h.sheet([ @@ -146,7 +144,7 @@ test("evaluate() is inapplicable to a text node that is not visible", async (t) test(`evaluate() is inapplicable to a text node that is excluded from the accessibility tree using the \`aria-hidden\` attribute`, async (t) => { - const document = Document.of( + const document = h.document( [<div aria-hidden="true">Hello world</div>], [ h.sheet([ @@ -163,7 +161,7 @@ test(`evaluate() is inapplicable to a text node that is excluded from the test(`evaluate() is inapplicable to a text node with a fixed absolute height set via the style attribute`, async (t) => { - const document = Document.of( + const document = h.document( [ <div style={{ @@ -186,7 +184,7 @@ test(`evaluate() is inapplicable to a text node with a fixed absolute height set }); test(`evaluate() is inapplicable to a text node with a fixed relative height`, async (t) => { - const document = Document.of( + const document = h.document( [<div>Hello world</div>], [ h.sheet([ diff --git a/packages/alfa-rules/test/sia-r84/rule.spec.tsx b/packages/alfa-rules/test/sia-r84/rule.spec.tsx index ca9f1864b8..73c62987b0 100644 --- a/packages/alfa-rules/test/sia-r84/rule.spec.tsx +++ b/packages/alfa-rules/test/sia-r84/rule.spec.tsx @@ -1,7 +1,6 @@ +import { h } from "@siteimprove/alfa-dom"; import { test } from "@siteimprove/alfa-test"; -import { Document } from "@siteimprove/alfa-dom"; - import R84, { Outcomes } from "../../src/sia-r84/rule"; import { evaluate } from "../common/evaluate"; @@ -14,7 +13,7 @@ test("evaluate() passes a scrollable element that is focusable", async (t) => { </div> ); - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R84, { document }), [ passed(R84, target, { @@ -30,7 +29,7 @@ test("evaluate() passes a scrollable element that has a focusable child", async </div> ); - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R84, { document }), [ passed(R84, target, { @@ -48,7 +47,7 @@ test("evaluate() passes a scrollable element that has a focusable descendant", a </div> ); - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R84, { document }), [ passed(R84, target, { @@ -63,7 +62,7 @@ test(`evaluate() fails a scrollable element that is neither focusable nor has <div style={{ height: "1.5em", overflow: "scroll" }}>Hello world</div> ); - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R84, { document }), [ failed(R84, target, { @@ -80,7 +79,7 @@ test(`evaluate() fails an element that restricts its width while making overflow </div> ); - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R84, { document }), [ failed(R84, target, { @@ -95,7 +94,7 @@ test(`evaluate() is inapplicable to an element that restricts its width while <div style={{ width: "200px", overflowX: "hidden" }}>Hello world</div> ); - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R84, { document }), [inapplicable(R84)]); }); @@ -106,7 +105,7 @@ test(`evaluate() is inapplicable to an element that restricts its height while <div style={{ height: "200px", overflowY: "hidden" }}>Hello world</div> ); - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R84, { document }), [inapplicable(R84)]); }); @@ -117,7 +116,7 @@ test(`evaluate() is inapplicable to an element that restricts its width while <div style={{ width: "200px", overflow: "scroll" }}>Hello world</div> ); - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R84, { document }), [inapplicable(R84)]); }); diff --git a/packages/alfa-rules/test/sia-r85/rule.spec.tsx b/packages/alfa-rules/test/sia-r85/rule.spec.tsx index 9c212a9c7f..5debec5c76 100644 --- a/packages/alfa-rules/test/sia-r85/rule.spec.tsx +++ b/packages/alfa-rules/test/sia-r85/rule.spec.tsx @@ -1,7 +1,6 @@ +import { h } from "@siteimprove/alfa-dom"; import { test } from "@siteimprove/alfa-test"; -import { Document } from "@siteimprove/alfa-dom"; - import R85, { Outcomes } from "../../src/sia-r85/rule"; import { evaluate } from "../common/evaluate"; @@ -10,7 +9,7 @@ import { passed, failed, inapplicable } from "../common/outcome"; test("evaluate() passes a paragraph whose text is not italic", async (t) => { const target = <p>Hello world</p>; - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R85, { document }), [ passed(R85, target, { @@ -22,7 +21,7 @@ test("evaluate() passes a paragraph whose text is not italic", async (t) => { test("evaluate() fails a paragraph whose text is italic", async (t) => { const target = <p style={{ fontStyle: "italic" }}>Hello world</p>; - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R85, { document }), [ failed(R85, target, { @@ -34,7 +33,7 @@ test("evaluate() fails a paragraph whose text is italic", async (t) => { test("evaluate() fails a paragraph whose text is italic by inheritance", async (t) => { const target = <p>Hello world</p>; - const document = Document.of([ + const document = h.document([ <div style={{ fontStyle: "italic" }}>{target}</div>, ]); @@ -52,7 +51,7 @@ test("evaluate() fails an ARIA paragraph whose text is italic", async (t) => { </div> ); - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R85, { document }), [ failed(R85, target, { @@ -68,7 +67,7 @@ test("evaluate() ignores a <p> element whose role was changed", async (t) => { </p> ); - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R85, { document }), [inapplicable(R85)]); }); diff --git a/packages/alfa-rules/test/sia-r86/rule.spec.tsx b/packages/alfa-rules/test/sia-r86/rule.spec.tsx index 0e005f089a..6848b6beaa 100644 --- a/packages/alfa-rules/test/sia-r86/rule.spec.tsx +++ b/packages/alfa-rules/test/sia-r86/rule.spec.tsx @@ -1,7 +1,6 @@ +import { h } from "@siteimprove/alfa-dom"; import { test } from "@siteimprove/alfa-test"; -import { Document } from "@siteimprove/alfa-dom"; - import R86, { Outcomes } from "../../src/sia-r86/rule"; import { evaluate } from "../common/evaluate"; @@ -11,7 +10,7 @@ test(`evaluate() passes an <img> element that is marked as decorative and not included in the accessibility tree`, async (t) => { const target = <img src="foo.jpg" alt="" />; - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R86, { document }), [ passed(R86, target, { @@ -24,7 +23,7 @@ test(`evaluate() fails an <img> element that is marked as decorative but is still included in the accessiblity tree`, async (t) => { const target = <img src="foo.jpg" alt="" aria-label="Foo" />; - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R86, { document }), [ failed(R86, target, { @@ -34,7 +33,7 @@ test(`evaluate() fails an <img> element that is marked as decorative but is }); test("evaluate() is inapplicabale to an <img> that is not marked as decorative", async (t) => { - const document = Document.of([<img src="foo.jpg" />]); + const document = h.document([<img src="foo.jpg" />]); t.deepEqual(await evaluate(R86, { document }), [inapplicable(R86)]); }); diff --git a/packages/alfa-rules/test/sia-r87/rule.spec.tsx b/packages/alfa-rules/test/sia-r87/rule.spec.tsx index 40ebcb2c5c..03c84d0abf 100644 --- a/packages/alfa-rules/test/sia-r87/rule.spec.tsx +++ b/packages/alfa-rules/test/sia-r87/rule.spec.tsx @@ -1,7 +1,6 @@ -import { h } from "@siteimprove/alfa-dom/h"; +import { h } from "@siteimprove/alfa-dom"; import { test } from "@siteimprove/alfa-test"; -import { Document } from "@siteimprove/alfa-dom"; import { Option } from "@siteimprove/alfa-option"; import R87, { Outcomes } from "../../src/sia-r87/rule"; @@ -12,7 +11,7 @@ import { oracle } from "../common/oracle"; test(`evaluate() passes a document whose first tabbable link references an element with a role of main`, async (t) => { - const document = Document.of([ + const document = h.document([ <html> <a href="#main">Skip to content</a> <main id="main">Content</main> @@ -30,7 +29,7 @@ test(`evaluate() passes a document whose first tabbable link references an element with a role of main`, async (t) => { const main = <main>Content</main>; - const document = Document.of([ + const document = h.document([ <html> <div tabindex="0" role="link"> Skip to content @@ -58,7 +57,7 @@ test(`evaluate() passes a document whose first tabbable link references an test(`evaluate() passes a document whose first tabbable link references an element that is determined to be the main content`, async (t) => { - const document = Document.of([ + const document = h.document([ <html> <a href="#main">Skip to content</a> <div id="main">Content</div> @@ -85,7 +84,7 @@ test(`evaluate() passes a document whose first tabbable link references an element that is determined to be the main content`, async (t) => { const main = <div>Content</div>; - const document = Document.of([ + const document = h.document([ <html> <div tabindex="0" role="link"> Skip to content @@ -113,7 +112,7 @@ test(`evaluate() passes a document whose first tabbable link references an }); test(`evaluate() fails a document without tabbable elements`, async (t) => { - const document = Document.of([ + const document = h.document([ <html> <main id="main">Content</main> </html>, @@ -128,7 +127,7 @@ test(`evaluate() fails a document without tabbable elements`, async (t) => { test(`evaluate() fails a document with a link that would be tabbable if not hidden`, async (t) => { - const document = Document.of([ + const document = h.document([ <html> <a href="#main" hidden> Skip to content @@ -146,7 +145,7 @@ test(`evaluate() fails a document with a link that would be tabbable if not test(`evaluate() fails a document whose first tabbable element is not a link`, async (t) => { - const document = Document.of([ + const document = h.document([ <html> <button /> <a href="#main">Skip to content</a> @@ -163,7 +162,7 @@ test(`evaluate() fails a document whose first tabbable element is not a test(`evaluate() fails a document whose first tabbable element is not a semantic link`, async (t) => { - const document = Document.of([ + const document = h.document([ <html> <a href="#main" role="button"> Skip to content @@ -181,7 +180,7 @@ test(`evaluate() fails a document whose first tabbable element is not a test(`evaluate() fails a document whose first tabbable link is not included in the accessibility tree`, async (t) => { - const document = Document.of([ + const document = h.document([ <html> <a href="#main" aria-hidden="true"> Skip to content @@ -198,7 +197,7 @@ test(`evaluate() fails a document whose first tabbable link is not included in }); test(`evaluate() fails a document whose first tabbable link is not visible`, async (t) => { - const document = Document.of( + const document = h.document( [ <html> <a href="#main">Skip to content</a> @@ -223,7 +222,7 @@ test(`evaluate() fails a document whose first tabbable link is not visible`, asy test(`evaluate() passes a document whose first tabbable link is visible when focused`, async (t) => { - const document = Document.of( + const document = h.document( [ <html> <a href="#main">Skip to content</a> @@ -252,7 +251,7 @@ test(`evaluate() passes a document whose first tabbable link is visible when test(`evaluates() passe a document whose first tabbable link references a container child at the start of main`, async (t) => { - const document = Document.of([ + const document = h.document([ <html> <a href="#content">Skip to content</a> @@ -273,7 +272,7 @@ test(`evaluates() passe a document whose first tabbable link references a test(`evaluates() passe a document whose first tabbable link references an empty child at the start of main`, async (t) => { - const document = Document.of([ + const document = h.document([ <html> <a href="#content">Skip to content</a> @@ -293,7 +292,7 @@ test(`evaluates() passe a document whose first tabbable link references an test(`evaluates() passe a document whose first tabbable link references a container around main`, async (t) => { - const document = Document.of([ + const document = h.document([ <html> <a href="#content">Skip to content</a> @@ -314,7 +313,7 @@ test(`evaluates() passe a document whose first tabbable link references a test(`evaluates() passe a document whose first tabbable link references an empty element before main`, async (t) => { - const document = Document.of([ + const document = h.document([ <html> <a href="#content">Skip to content</a> diff --git a/packages/alfa-rules/test/sia-r9/rule.spec.tsx b/packages/alfa-rules/test/sia-r9/rule.spec.tsx index 2d662eaedc..6728f10bd1 100644 --- a/packages/alfa-rules/test/sia-r9/rule.spec.tsx +++ b/packages/alfa-rules/test/sia-r9/rule.spec.tsx @@ -1,7 +1,6 @@ +import { h } from "@siteimprove/alfa-dom/h"; import { test } from "@siteimprove/alfa-test"; -import { Document } from "@siteimprove/alfa-dom"; - import R9 from "../../src/sia-r9/rule"; import { RefreshDelay as Outcomes } from "../../src/common/outcome/refresh-delay"; @@ -13,7 +12,7 @@ test("evaluates() passes when there is an immediate refresh", async (t) => { <meta http-equiv="refresh" content="0; URL='https://w3c.org'" /> ); - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R9, { document }), [ passed(R9, target, { 1: Outcomes.HasImmediateRefresh }), @@ -25,7 +24,7 @@ test("evaluates() passes when there is a refresh after 20h", async (t) => { <meta http-equiv="refresh" content="72001; URL='https://w3c.org'" /> ); - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R9, { document }), [ passed(R9, target, { 1: Outcomes.HasTwentyHoursDelayedRefresh }), @@ -37,7 +36,7 @@ test("evaluates() fails when there is a delayed refresh", async (t) => { <meta http-equiv="refresh" content="30; URL='https://w3c.org'" /> ); - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R9, { document }), [ failed(R9, target, { 1: Outcomes.HasDelayedRefresh }), @@ -52,7 +51,7 @@ test("evaluates() only considers the first <meta> element", async (t) => { <meta http-equiv="refresh" content="30; URL='https://w3c.org'" /> ); - const document = Document.of([target, ignored]); + const document = h.document([target, ignored]); t.deepEqual(await evaluate(R9, { document }), [ passed(R9, target, { 1: Outcomes.HasImmediateRefresh }), @@ -60,20 +59,20 @@ test("evaluates() only considers the first <meta> element", async (t) => { }); test("evaluate() is inapplicable when there is no <meta> element", async (t) => { - const document = Document.of([<div>Foo</div>]); + const document = h.document([<div>Foo</div>]); t.deepEqual(await evaluate(R9, { document }), [inapplicable(R9)]); }); test("evaluate() is inapplicable when there is no <meta refresh> element", async (t) => { - const document = Document.of([<meta content="30" />]); + const document = h.document([<meta content="30" />]); t.deepEqual(await evaluate(R9, { document }), [inapplicable(R9)]); }); test("evaluate() is inapplicable when the content attribute is invalid", async (t) => { // ':' instead of ';' - const document = Document.of([ + const document = h.document([ <meta http-equiv="refresh" content="0: URL='https://w3c.org'" />, ]); diff --git a/packages/alfa-rules/test/sia-r90/rule.spec.tsx b/packages/alfa-rules/test/sia-r90/rule.spec.tsx index b0ee231160..6bbc9af17b 100644 --- a/packages/alfa-rules/test/sia-r90/rule.spec.tsx +++ b/packages/alfa-rules/test/sia-r90/rule.spec.tsx @@ -1,4 +1,4 @@ -import { Document } from "@siteimprove/alfa-dom"; +import { h } from "@siteimprove/alfa-dom"; import { test } from "@siteimprove/alfa-test"; import R90, { Outcomes } from "../../src/sia-r90/rule"; @@ -9,7 +9,7 @@ import { passed, failed, inapplicable } from "../common/outcome"; test("evaluate() passes a button with only text node children", async (t) => { const target = <button>Foo</button>; - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R90, { document }), [ passed(R90, target, { 1: Outcomes.HasNoTabbableDescendants }), @@ -23,7 +23,7 @@ test("evaluate() passes a button with a span child", async (t) => { </button> ); - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R90, { document }), [ passed(R90, target, { 1: Outcomes.HasNoTabbableDescendants }), @@ -37,7 +37,7 @@ test("evaluate() fails a button with a link child", async (t) => { </button> ); - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R90, { document }), [ failed(R90, target, { 1: Outcomes.HasTabbableDescendants }), @@ -51,7 +51,7 @@ test("evaluate() fails an ARIA button with a link child", async (t) => { </span> ); - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R90, { document }), [ failed(R90, target, { 1: Outcomes.HasTabbableDescendants }), @@ -59,7 +59,7 @@ test("evaluate() fails an ARIA button with a link child", async (t) => { }); test("evaluate() is inapplicable if there is no role with presentational children", async (t) => { - const document = Document.of([ + const document = h.document([ <div> <span>Foo</span> </div>, diff --git a/packages/alfa-rules/test/sia-r91/rule.spec.tsx b/packages/alfa-rules/test/sia-r91/rule.spec.tsx index 8e5ee5abef..df3058e416 100644 --- a/packages/alfa-rules/test/sia-r91/rule.spec.tsx +++ b/packages/alfa-rules/test/sia-r91/rule.spec.tsx @@ -1,8 +1,6 @@ -import { h } from "@siteimprove/alfa-dom/h"; +import { h } from "@siteimprove/alfa-dom"; import { test } from "@siteimprove/alfa-test"; -import { Document } from "@siteimprove/alfa-dom"; - import R91, { Outcomes } from "../../src/sia-r91/rule"; import { evaluate } from "../common/evaluate"; @@ -11,7 +9,7 @@ import { passed, failed, inapplicable } from "../common/outcome"; test("evaluate() passes on non important style", async (t) => { const target = <div style={{ letterSpacing: "0.1em" }}>Hello World</div>; - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R91, { document }), [ passed(R91, target, { @@ -25,7 +23,7 @@ test("evaluate() passes on large enough value", async (t) => { <div style={{ letterSpacing: "0.12em !important" }}>Hello World</div> ); - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R91, { document }), [ passed(R91, target, { @@ -37,7 +35,7 @@ test("evaluate() passes on large enough value", async (t) => { test("evaluate() passes on important cascaded styles", async (t) => { const target = <div style={{ letterSpacing: "0.15em" }}>Hello World</div>; - const document = Document.of( + const document = h.document( [target], [h.sheet([h.rule.style("div", { letterSpacing: "0.1em !important" })])] ); @@ -54,7 +52,7 @@ test("evaluate() fails on important small values", async (t) => { <div style={{ letterSpacing: "0.1em !important" }}>Hello World</div> ); - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R91, { document }), [ failed(R91, target, { @@ -64,7 +62,7 @@ test("evaluate() fails on important small values", async (t) => { }); test("evaluate() is inapplicable if letter-spacing is not declared in the style", async (t) => { - const document = Document.of([ + const document = h.document([ <div style={{ color: "red" }}>Hello World</div>, ]); diff --git a/packages/alfa-rules/test/sia-r92/rule.spec.tsx b/packages/alfa-rules/test/sia-r92/rule.spec.tsx index 20edaf7fe9..6e138dbc0c 100644 --- a/packages/alfa-rules/test/sia-r92/rule.spec.tsx +++ b/packages/alfa-rules/test/sia-r92/rule.spec.tsx @@ -1,8 +1,6 @@ -import { h } from "@siteimprove/alfa-dom/h"; +import { h } from "@siteimprove/alfa-dom"; import { test } from "@siteimprove/alfa-test"; -import { Document } from "@siteimprove/alfa-dom"; - import R92, { Outcomes } from "../../src/sia-r92/rule"; import { evaluate } from "../common/evaluate"; @@ -11,7 +9,7 @@ import { passed, failed, inapplicable } from "../common/outcome"; test("evaluate() passes on non important style", async (t) => { const target = <div style={{ wordSpacing: "0.1em" }}>Hello World</div>; - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R92, { document }), [ passed(R92, target, { @@ -25,7 +23,7 @@ test("evaluate() passes on large enough value", async (t) => { <div style={{ wordSpacing: "0.16em !important" }}>Hello World</div> ); - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R92, { document }), [ passed(R92, target, { @@ -37,7 +35,7 @@ test("evaluate() passes on large enough value", async (t) => { test("evaluate() passes on important cascaded styles", async (t) => { const target = <div style={{ wordSpacing: "0.18em" }}>Hello World</div>; - const document = Document.of( + const document = h.document( [target], [h.sheet([h.rule.style("div", { wordSpacing: "0.1em !important" })])] ); @@ -54,7 +52,7 @@ test("evaluate() fails on important small values", async (t) => { <div style={{ wordSpacing: "0.1em !important" }}>Hello World</div> ); - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R92, { document }), [ failed(R92, target, { @@ -64,7 +62,7 @@ test("evaluate() fails on important small values", async (t) => { }); test("evaluate() is inapplicable if word-spacing is not declared in the style", async (t) => { - const document = Document.of([ + const document = h.document([ <div style={{ color: "red" }}>Hello World</div>, ]); diff --git a/packages/alfa-rules/test/sia-r93/rule.spec.tsx b/packages/alfa-rules/test/sia-r93/rule.spec.tsx index f53bcf2b9f..a5bbd2903a 100644 --- a/packages/alfa-rules/test/sia-r93/rule.spec.tsx +++ b/packages/alfa-rules/test/sia-r93/rule.spec.tsx @@ -1,8 +1,6 @@ -import { h } from "@siteimprove/alfa-dom/h"; +import { h } from "@siteimprove/alfa-dom"; import { test } from "@siteimprove/alfa-test"; -import { Document } from "@siteimprove/alfa-dom"; - import R93, { Outcomes } from "../../src/sia-r93/rule"; import { evaluate } from "../common/evaluate"; @@ -11,7 +9,7 @@ import { passed, failed, inapplicable } from "../common/outcome"; test("evaluate() passes on non important style", async (t) => { const target = <div style={{ lineHeight: "1em" }}>Hello World</div>; - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R93, { document }), [ passed(R93, target, { @@ -25,7 +23,7 @@ test("evaluate() passes on large enough value", async (t) => { <div style={{ lineHeight: "1.5em !important" }}>Hello World</div> ); - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R93, { document }), [ passed(R93, target, { @@ -37,7 +35,7 @@ test("evaluate() passes on large enough value", async (t) => { test("evaluate() passes on important cascaded styles", async (t) => { const target = <div style={{ lineHeight: "1.5em" }}>Hello World</div>; - const document = Document.of( + const document = h.document( [target], [h.sheet([h.rule.style("div", { lineHeight: "1em !important" })])] ); @@ -54,7 +52,7 @@ test("evaluate() fails on important small values", async (t) => { <div style={{ lineHeight: "1em !important" }}>Hello World</div> ); - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R93, { document }), [ failed(R93, target, { @@ -64,7 +62,7 @@ test("evaluate() fails on important small values", async (t) => { }); test("evaluate() is inapplicable if line-height is not declared in the style", async (t) => { - const document = Document.of([ + const document = h.document([ <div style={{ color: "red" }}>Hello World</div>, ]); diff --git a/packages/alfa-rules/test/sia-r94/rule.spec.tsx b/packages/alfa-rules/test/sia-r94/rule.spec.tsx index 61be5282ce..898922c4f9 100644 --- a/packages/alfa-rules/test/sia-r94/rule.spec.tsx +++ b/packages/alfa-rules/test/sia-r94/rule.spec.tsx @@ -1,4 +1,4 @@ -import { Document } from "@siteimprove/alfa-dom"; +import { h } from "@siteimprove/alfa-dom"; import { test } from "@siteimprove/alfa-test"; import R94, { Outcomes } from "../../src/sia-r94/rule"; @@ -9,7 +9,7 @@ import { passed, failed, inapplicable } from "../common/outcome"; test("evaluate() passes a menuitem with an accessible name", async (t) => { const target = <li role="menuitem">Foo</li>; - const document = Document.of([<ul role="menu">{target}</ul>]); + const document = h.document([<ul role="menu">{target}</ul>]); t.deepEqual(await evaluate(R94, { document }), [ passed(R94, target, { 1: Outcomes.HasName }), @@ -23,7 +23,7 @@ test("evaluate() fails a menuitem with no accessible name", async (t) => { </li> ); - const document = Document.of([<ul role="menu">{target}</ul>]); + const document = h.document([<ul role="menu">{target}</ul>]); t.deepEqual(await evaluate(R94, { document }), [ failed(R94, target, { 1: Outcomes.HasNoName }), @@ -31,7 +31,7 @@ test("evaluate() fails a menuitem with no accessible name", async (t) => { }); test("evaluate() is inapplicable when there is no menuitem", async (t) => { - const document = Document.of([ + const document = h.document([ <menu> <li>Foo</li> </menu>, @@ -41,7 +41,7 @@ test("evaluate() is inapplicable when there is no menuitem", async (t) => { }); test("evaluate() is inapplicable on menuitem that are not exposed", async (t) => { - const document = Document.of([ + const document = h.document([ <ul role="menu" hidden> <li role="menuitem">Foo</li> </ul>, diff --git a/packages/alfa-rules/test/sia-r95/rule.spec.tsx b/packages/alfa-rules/test/sia-r95/rule.spec.tsx index 58235105d1..8a75de623d 100644 --- a/packages/alfa-rules/test/sia-r95/rule.spec.tsx +++ b/packages/alfa-rules/test/sia-r95/rule.spec.tsx @@ -1,4 +1,4 @@ -import { Document } from "@siteimprove/alfa-dom"; +import { h } from "@siteimprove/alfa-dom"; import { test } from "@siteimprove/alfa-test"; import R95, { Outcomes } from "../../src/sia-r95/rule"; @@ -8,10 +8,10 @@ import { passed, failed, inapplicable } from "../common/outcome"; test("evaluate() passes an iframe with negative tabindex and no interactive content", async (t) => { const target = ( - <iframe tabindex="-1">{Document.of([<p>Hello World!</p>])}</iframe> + <iframe tabindex="-1">{h.document([<p>Hello World!</p>])}</iframe> ); - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R95, { document }), [ passed(R95, target, { 1: Outcomes.HasNoInteractiveElement }), @@ -21,11 +21,11 @@ test("evaluate() passes an iframe with negative tabindex and no interactive cont test("evaluate() passes an iframe with negative tabindex and a non-tabbable interactive element", async (t) => { const target = ( <iframe tabindex="-1"> - {Document.of([<button tabindex="-1">Hello World!</button>])} + {h.document([<button tabindex="-1">Hello World!</button>])} </iframe> ); - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R95, { document }), [ passed(R95, target, { 1: Outcomes.HasNoInteractiveElement }), @@ -35,11 +35,11 @@ test("evaluate() passes an iframe with negative tabindex and a non-tabbable inte test("evaluate() passes an iframe with negative tabindex and an invisible interactive element", async (t) => { const target = ( <iframe tabindex="-1"> - {Document.of([<button hidden>Hello World!</button>])} + {h.document([<button hidden>Hello World!</button>])} </iframe> ); - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R95, { document }), [ passed(R95, target, { 1: Outcomes.HasNoInteractiveElement }), @@ -49,11 +49,11 @@ test("evaluate() passes an iframe with negative tabindex and an invisible intera test("evaluate() passes an invisible iframe with negative tabindex and an interactive element", async (t) => { const target = ( <iframe tabindex="-1" hidden> - {Document.of([<button>Hello World!</button>])} + {h.document([<button>Hello World!</button>])} </iframe> ); - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R95, { document }), [ passed(R95, target, { 1: Outcomes.HasNoInteractiveElement }), @@ -62,12 +62,10 @@ test("evaluate() passes an invisible iframe with negative tabindex and an intera test("evaluate() fails an iframe with negative tabindex and an interactive element", async (t) => { const target = ( - <iframe tabindex="-1"> - {Document.of([<button>Hello World!</button>])} - </iframe> + <iframe tabindex="-1">{h.document([<button>Hello World!</button>])}</iframe> ); - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R95, { document }), [ failed(R95, target, { 1: Outcomes.HasInteractiveElement }), @@ -75,25 +73,23 @@ test("evaluate() fails an iframe with negative tabindex and an interactive eleme }); test("evaluate() is inapplicable on an iframe with non-negative tabindex", async (t) => { - const document = Document.of([ - <iframe tabindex="0"> - {Document.of([<button>Hello World!</button>])} - </iframe>, + const document = h.document([ + <iframe tabindex="0">{h.document([<button>Hello World!</button>])}</iframe>, ]); t.deepEqual(await evaluate(R95, { document }), [inapplicable(R95)]); }); test("evaluate() is inapplicable on an iframe with no tabindex", async (t) => { - const document = Document.of([ - <iframe>{Document.of([<button>Hello World!</button>])}</iframe>, + const document = h.document([ + <iframe>{h.document([<button>Hello World!</button>])}</iframe>, ]); t.deepEqual(await evaluate(R95, { document }), [inapplicable(R95)]); }); test("evaluate() is inapplicable when there is no iframe", async (t) => { - const document = Document.of([<button>Hello World!</button>]); + const document = h.document([<button>Hello World!</button>]); t.deepEqual(await evaluate(R95, { document }), [inapplicable(R95)]); }); diff --git a/packages/alfa-rules/test/sia-r96/rule.spec.tsx b/packages/alfa-rules/test/sia-r96/rule.spec.tsx index 0105f1320d..48ed438c98 100644 --- a/packages/alfa-rules/test/sia-r96/rule.spec.tsx +++ b/packages/alfa-rules/test/sia-r96/rule.spec.tsx @@ -1,7 +1,6 @@ +import { h } from "@siteimprove/alfa-dom"; import { test } from "@siteimprove/alfa-test"; -import { Document } from "@siteimprove/alfa-dom"; - import R96 from "../../src/sia-r96/rule"; import { RefreshDelay as Outcomes } from "../../src/common/outcome/refresh-delay"; @@ -13,7 +12,7 @@ test("evaluates() passes when there is an immediate refresh", async (t) => { <meta http-equiv="refresh" content="0; URL='https://w3c.org'" /> ); - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R96, { document }), [ passed(R96, target, { 1: Outcomes.HasImmediateRefresh }), @@ -25,7 +24,7 @@ test("evaluates() fails when there is a delayed refresh", async (t) => { <meta http-equiv="refresh" content="30; URL='https://w3c.org'" /> ); - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R96, { document }), [ failed(R96, target, { 1: Outcomes.HasDelayedRefresh }), @@ -37,7 +36,7 @@ test("evaluates() fails when there is a refresh after 20h", async (t) => { <meta http-equiv="refresh" content="72001; URL='https://w3c.org'" /> ); - const document = Document.of([target]); + const document = h.document([target]); t.deepEqual(await evaluate(R96, { document }), [ failed(R96, target, { 1: Outcomes.HasDelayedRefresh }), @@ -52,7 +51,7 @@ test("evaluates() only considers the first <meta> element", async (t) => { <meta http-equiv="refresh" content="30; URL='https://w3c.org'" /> ); - const document = Document.of([target, ignored]); + const document = h.document([target, ignored]); t.deepEqual(await evaluate(R96, { document }), [ passed(R96, target, { 1: Outcomes.HasImmediateRefresh }), @@ -60,20 +59,20 @@ test("evaluates() only considers the first <meta> element", async (t) => { }); test("evaluate() is inapplicable when there is no <meta> element", async (t) => { - const document = Document.of([<div>Foo</div>]); + const document = h.document([<div>Foo</div>]); t.deepEqual(await evaluate(R96, { document }), [inapplicable(R96)]); }); test("evaluate() is inapplicable when there is no <meta refresh> element", async (t) => { - const document = Document.of([<meta content="30" />]); + const document = h.document([<meta content="30" />]); t.deepEqual(await evaluate(R96, { document }), [inapplicable(R96)]); }); test("evaluate() is inapplicable when the content attribute is invalid", async (t) => { // ':' instead of ';' - const document = Document.of([ + const document = h.document([ <meta http-equiv="refresh" content="0: URL='https://w3c.org'" />, ]); diff --git a/packages/alfa-rules/tsconfig.json b/packages/alfa-rules/tsconfig.json index 34badac221..4290744850 100644 --- a/packages/alfa-rules/tsconfig.json +++ b/packages/alfa-rules/tsconfig.json @@ -17,6 +17,7 @@ "src/common/expectation/media-text-alternative.ts", "src/common/expectation/video-description-track-accurate.ts", "src/common/group.ts", + "src/common/normalize.ts", "src/common/outcome/contrast.ts", "src/common/outcome/text-spacing.ts", "src/common/outcome/refresh-delay.ts", @@ -111,6 +112,7 @@ "src/sia-r5/rule.ts", "src/sia-r50/rule.ts", "src/sia-r53/rule.ts", + "src/sia-r56/rule.ts", "src/sia-r57/rule.ts", "src/sia-r59/rule.ts", "src/sia-r6/rule.ts", @@ -151,23 +153,37 @@ "test/common/outcome.ts", "test/common/predicate/is-at-the-start.spec.tsx", "test/common/predicate/is-visible.spec.tsx", + "test/common/predicate/has-role.spec.tsx", "test/common/expectation/get-colors.spec.tsx", "test/sia-r1/rule.spec.tsx", "test/sia-r2/rule.spec.tsx", + "test/sia-r3/rule.spec.tsx", + "test/sia-r4/rule.spec.tsx", + "test/sia-r5/rule.spec.tsx", + "test/sia-r6/rule.spec.tsx", "test/sia-r7/rule.spec.tsx", + "test/sia-r8/rule.spec.tsx", "test/sia-r9/rule.spec.tsx", "test/sia-r10/rule.spec.tsx", + "test/sia-r11/rule.spec.tsx", + "test/sia-r12/rule.spec.tsx", "test/sia-r13/rule.spec.tsx", "test/sia-r14/rule.spec.tsx", "test/sia-r15/rule.spec.tsx", "test/sia-r16/rule.spec.tsx", + "test/sia-r17/rule.spec.tsx", + "test/sia-r18/rule.spec.tsx", + "test/sia-r19/rule.spec.tsx", + "test/sia-r20/rule.spec.tsx", "test/sia-r21/rule.spec.tsx", "test/sia-r24/rule.spec.tsx", "test/sia-r38/rule.spec.tsx", "test/sia-r41/rule.spec.tsx", + "test/sia-r42/rule.spec.tsx", "test/sia-r45/rule.spec.tsx", "test/sia-r46/rule.spec.tsx", "test/sia-r53/rule.spec.tsx", + "test/sia-r56/rule.spec.tsx", "test/sia-r57/rule.spec.tsx", "test/sia-r61/rule.spec.tsx", "test/sia-r62/rule.spec.tsx", diff --git a/packages/alfa-selector/src/selector.ts b/packages/alfa-selector/src/selector.ts index 5d4d5100ed..d2b18ae566 100644 --- a/packages/alfa-selector/src/selector.ts +++ b/packages/alfa-selector/src/selector.ts @@ -1446,12 +1446,24 @@ export namespace Selector { return this._index.matches(indices.get(element)!); } + public equals(value: NthChild): boolean; + + public equals(value: unknown): value is this; + + public equals(value: unknown): boolean { + return value instanceof NthChild && value._index.equals(this._index); + } + public toJSON(): NthChild.JSON { return { ...super.toJSON(), index: this._index.toJSON(), }; } + + public toString(): string { + return `:${this.name}(${this._index})`; + } } export namespace NthChild { @@ -1494,12 +1506,24 @@ export namespace Selector { return this._index.matches(indices.get(element)!); } + public equals(value: NthLastChild): boolean; + + public equals(value: unknown): value is this; + + public equals(value: unknown): boolean { + return value instanceof NthLastChild && value._index.equals(this._index); + } + public toJSON(): NthLastChild.JSON { return { ...super.toJSON(), index: this._index.toJSON(), }; } + + public toString(): string { + return `:${this.name}(${this._index})`; + } } export namespace NthLastChild { @@ -1601,12 +1625,24 @@ export namespace Selector { return this._index.matches(indices.get(element)!); } + public equals(value: NthOfType): boolean; + + public equals(value: unknown): value is this; + + public equals(value: unknown): boolean { + return value instanceof NthOfType && value._index.equals(this._index); + } + public toJSON(): NthOfType.JSON { return { ...super.toJSON(), index: this._index.toJSON(), }; } + + public toString(): string { + return `:${this.name}(${this._index})`; + } } export namespace NthOfType { @@ -1650,12 +1686,24 @@ export namespace Selector { return this._index.matches(indices.get(element)!); } + public equals(value: NthLastOfType): boolean; + + public equals(value: unknown): value is this; + + public equals(value: unknown): boolean { + return value instanceof NthLastOfType && value._index.equals(this._index); + } + public toJSON(): NthLastOfType.JSON { return { ...super.toJSON(), index: this._index.toJSON(), }; } + + public toString(): string { + return `:${this.name}(${this._index})`; + } } export namespace NthLastOfType { @@ -1802,6 +1850,12 @@ export namespace Selector { selector: this._selector.toJSON(), }; } + + public toString(): string { + return `::${this.name}` + this._selector.isSome() + ? `(${this._selector})` + : ""; + } } export namespace Cue { @@ -1843,6 +1897,12 @@ export namespace Selector { selector: this._selector.toJSON(), }; } + + public toString(): string { + return `::${this.name}` + this._selector.isSome() + ? `(${this._selector})` + : ""; + } } export namespace CueRegion { @@ -1949,6 +2009,10 @@ export namespace Selector { idents: Array.toJSON(this._idents), }; } + + public toString(): string { + return `::${this.name}(${this._idents})`; + } } export namespace Part { @@ -2019,6 +2083,10 @@ export namespace Selector { selectors: Array.toJSON(this._selectors), }; } + + public toString(): string { + return `::${this.name}(${this._selectors})`; + } } export namespace Slotted { diff --git a/packages/alfa-style/src/index.ts b/packages/alfa-style/src/index.ts index 6071b98438..bcffecc981 100644 --- a/packages/alfa-style/src/index.ts +++ b/packages/alfa-style/src/index.ts @@ -87,6 +87,12 @@ import "./property/font-family"; import "./property/font-size"; import "./property/font-stretch"; import "./property/font-style"; +import "./property/font-variant"; +import "./property/font-variant-caps"; +import "./property/font-variant-east-asian"; +import "./property/font-variant-ligatures"; +import "./property/font-variant-numeric"; +import "./property/font-variant-position"; import "./property/font-weight"; import "./property/height"; import "./property/inset"; @@ -115,6 +121,7 @@ import "./property/text-decoration"; import "./property/text-decoration-color"; import "./property/text-decoration-line"; import "./property/text-decoration-style"; +import "./property/text-decoration-thickness"; import "./property/text-indent"; import "./property/text-overflow"; import "./property/text-transform"; diff --git a/packages/alfa-style/src/property/background-size.ts b/packages/alfa-style/src/property/background-size.ts index c98306873c..516cd4cd9f 100644 --- a/packages/alfa-style/src/property/background-size.ts +++ b/packages/alfa-style/src/property/background-size.ts @@ -1,13 +1,15 @@ import { Keyword, Length, Percentage, Token } from "@siteimprove/alfa-css"; import { Iterable } from "@siteimprove/alfa-iterable"; import { Parser } from "@siteimprove/alfa-parser"; +import { Slice } from "@siteimprove/alfa-slice"; import { Property } from "../property"; import { Resolver } from "../resolver"; import { List } from "./value/list"; +import { Tuple } from "./value/tuple"; -const { map, either, delimited, option, pair, separatedList } = Parser; +const { map, either, delimited, option, pair, right, separatedList } = Parser; declare module "../property" { interface Longhands { @@ -24,11 +26,10 @@ export type Specified = List<Specified.Item>; * @internal */ export namespace Specified { + export type Dimension = Length | Percentage | Keyword<"auto">; + export type Item = - | [ - Length | Percentage | Keyword<"auto">, - Length | Percentage | Keyword<"auto"> - ] + | Tuple<[Dimension, Dimension]> | Keyword<"cover"> | Keyword<"contain">; } @@ -42,24 +43,35 @@ export type Computed = List<Computed.Item>; * @internal */ export namespace Computed { + export type Dimension = Length<"px"> | Percentage | Keyword<"auto">; + export type Item = - | [ - Length<"px"> | Percentage | Keyword<"auto">, - Length<"px"> | Percentage | Keyword<"auto"> - ] + | Tuple<[Dimension, Dimension]> | Keyword<"cover"> | Keyword<"contain">; } +/** + * @internal + */ +const parseDimension = either<Slice<Token>, Specified.Dimension, string>( + Length.parse, + Percentage.parse, + Keyword.parse("auto") +); + /** * @internal */ export const parse = either( - pair( - either(Length.parse, Keyword.parse("auto")), - map(option(either(Length.parse, Keyword.parse("auto"))), (y) => - y.getOrElse(() => Keyword.of("auto")) - ) + map( + pair( + parseDimension, + map(option(right(Token.parseWhitespace, parseDimension)), (y) => + y.getOrElse(() => Keyword.of("auto")) + ) + ), + ([x, y]) => Tuple.of(x, y) ), Keyword.parse("contain", "cover") ); @@ -82,7 +94,7 @@ export const parseList = map( export default Property.register( "background-size", Property.of<Specified, Computed>( - List.of([[Keyword.of("auto"), Keyword.of("auto")]], ", "), + List.of([Tuple.of(Keyword.of("auto"), Keyword.of("auto"))], ", "), parseList, (value, style) => value.map((sizes) => @@ -92,12 +104,12 @@ export default Property.register( return size; } - const [x, y] = size; + const [x, y] = size.values; - return [ + return Tuple.of( x.type === "length" ? Resolver.length(x, style) : x, - y.type === "length" ? Resolver.length(y, style) : y, - ]; + y.type === "length" ? Resolver.length(y, style) : y + ); }), ", " ) diff --git a/packages/alfa-style/src/property/background.ts b/packages/alfa-style/src/property/background.ts index ec88e9f114..d8ac64e387 100644 --- a/packages/alfa-style/src/property/background.ts +++ b/packages/alfa-style/src/property/background.ts @@ -6,6 +6,7 @@ import { Slice } from "@siteimprove/alfa-slice"; import { Property } from "../property"; import { List } from "./value/list"; +import { Tuple } from "./value/tuple"; import * as Attachment from "./background-attachment"; import * as Clip from "./background-clip"; @@ -253,7 +254,7 @@ export default Property.registerShorthand( image.push(layer[1] ?? Keyword.of("none")); positionX.push(layer[2] ?? Percentage.of(0)); positionY.push(layer[3] ?? Percentage.of(0)); - size.push(layer[4] ?? [Keyword.of("auto"), Keyword.of("auto")]); + size.push(layer[4] ?? Tuple.of(Keyword.of("auto"), Keyword.of("auto"))); repeatX.push(layer[5] ?? Keyword.of("repeat")); repeatY.push(layer[6] ?? Keyword.of("repeat")); attachment.push(layer[7] ?? Keyword.of("scroll")); diff --git a/packages/alfa-style/src/property/font-variant-caps.ts b/packages/alfa-style/src/property/font-variant-caps.ts new file mode 100644 index 0000000000..2bca7a98be --- /dev/null +++ b/packages/alfa-style/src/property/font-variant-caps.ts @@ -0,0 +1,53 @@ +import { Keyword } from "@siteimprove/alfa-css"; + +import { Property } from "../property"; + +declare module "../property" { + interface Longhands { + "font-variant-caps": Property<Specified, Computed>; + } +} + +/** + * @internal + */ +export type Specified = + | Keyword<"normal"> + | Keyword<"small-caps"> + | Keyword<"all-small-caps"> + | Keyword<"petite-caps"> + | Keyword<"all-petite-caps"> + | Keyword<"unicase"> + | Keyword<"titling-caps">; + +/** + * @internal + */ +export type Computed = Specified; + +/** + * @internal + */ +export const parse = Keyword.parse( + "normal", + "small-caps", + "all-small-caps", + "petite-caps", + "all-petite-caps", + "unicase", + "titling-caps" +); + +/** + * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/font-variant-caps} + * @internal + */ +export default Property.register( + "font-variant-caps", + Property.of<Specified, Computed>( + Keyword.of("normal"), + parse, + (position) => position, + { inherits: true } + ) +); diff --git a/packages/alfa-style/src/property/font-variant-east-asian.ts b/packages/alfa-style/src/property/font-variant-east-asian.ts new file mode 100644 index 0000000000..1d5402982c --- /dev/null +++ b/packages/alfa-style/src/property/font-variant-east-asian.ts @@ -0,0 +1,138 @@ +import { Keyword, Token } from "@siteimprove/alfa-css"; +import { Parser } from "@siteimprove/alfa-parser"; +import { Err, Result } from "@siteimprove/alfa-result"; +import { Slice } from "@siteimprove/alfa-slice"; + +import { Property } from "../property"; +import { List } from "./value/list"; + +const { either } = Parser; + +declare module "../property" { + interface Longhands { + "font-variant-east-asian": Property<Specified, Computed>; + } +} + +/** + * @internal + */ +export type Specified = Keyword<"normal"> | List<Specified.Item>; + +/** + * @internal + */ +export namespace Specified { + export type Variant = + | Keyword<"jis78"> + | Keyword<"jis83"> + | Keyword<"jis90"> + | Keyword<"jis04"> + | Keyword<"simplified"> + | Keyword<"traditional">; + + export type Width = Keyword<"proportional-width"> | Keyword<"full-width">; + + export type Item = Variant | Width | Keyword<"ruby">; +} +/** + * @internal + */ +export type Computed = Specified; + +/** + * @internal + */ +export const parseVariant = Keyword.parse( + "jis78", + "jis83", + "jis90", + "jis04", + "simplified", + "traditional" +); + +/** + * @internal + */ +export const parseWidth = Keyword.parse("proportional-width", "full-width"); + +/** + * @internal + */ +const parseEastAsian: Parser<Slice<Token>, List<Specified.Item>, string> = ( + input +) => { + let variant: Specified.Variant | undefined; + let width: Specified.Width | undefined; + let ruby: Keyword<"ruby"> | undefined; + + while (true) { + for (const [remainder] of Token.parseWhitespace(input)) { + input = remainder; + } + + if (variant === undefined) { + const result = parseVariant(input); + + if (result.isOk()) { + [input, variant] = result.get(); + continue; + } + } + + if (width === undefined) { + const result = parseWidth(input); + + if (result.isOk()) { + [input, width] = result.get(); + continue; + } + } + + if (ruby === undefined) { + const result = Keyword.parse("ruby")(input); + + if (result.isOk()) { + [input, ruby] = result.get(); + continue; + } + } + + break; + } + + if (variant === undefined && width === undefined && ruby === undefined) { + return Err.of("At least one East Asian variant value must be provided"); + } + + return Result.of([ + input, + List.of( + [variant, width, ruby].filter( + (value) => value !== undefined + // filter doesn't narrow so we need to do it manually + ) as Array<Specified.Item>, + " " + ), + ]); +}; + +/** + * @internal + */ +export const parse = either(Keyword.parse("normal"), parseEastAsian); + +/** + * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/font-variant-east-asian} + * @internal + */ +export default Property.register( + "font-variant-east-asian", + Property.of<Specified, Computed>( + Keyword.of("normal"), + parse, + (numeric) => numeric, + { inherits: true } + ) +); diff --git a/packages/alfa-style/src/property/font-variant-ligatures.ts b/packages/alfa-style/src/property/font-variant-ligatures.ts new file mode 100644 index 0000000000..973452a766 --- /dev/null +++ b/packages/alfa-style/src/property/font-variant-ligatures.ts @@ -0,0 +1,172 @@ +import { Keyword, Token } from "@siteimprove/alfa-css"; +import { Parser } from "@siteimprove/alfa-parser"; +import { Err, Result } from "@siteimprove/alfa-result"; +import { Slice } from "@siteimprove/alfa-slice"; + +import { Property } from "../property"; +import { List } from "./value/list"; + +const { either } = Parser; + +declare module "../property" { + interface Longhands { + "font-variant-ligatures": Property<Specified, Computed>; + } +} + +/** + * @internal + */ +export type Specified = + | Keyword<"none"> + | Keyword<"normal"> + | List<Specified.Item>; + +/** + * @internal + */ +export namespace Specified { + export type Common = + | Keyword<"common-ligatures"> + | Keyword<"no-common-ligatures">; + + export type Discretionary = + | Keyword<"discretionary-ligatures"> + | Keyword<"no-discretionary-ligatures">; + + export type Historical = + | Keyword<"historical-ligatures"> + | Keyword<"no-historical-ligatures">; + + export type Contextual = Keyword<"contextual"> | Keyword<"no-contextual">; + + export type Item = Common | Discretionary | Historical | Contextual; +} +/** + * @internal + */ +export type Computed = Specified; + +/** + * @internal + */ +export const parseCommon = Keyword.parse( + "common-ligatures", + "no-common-ligatures" +); + +/** + * @internal + */ +export const parseDiscretionary = Keyword.parse( + "discretionary-ligatures", + "no-discretionary-ligatures" +); + +/** + * @internal + */ +export const parseHistorical = Keyword.parse( + "historical-ligatures", + "no-historical-ligatures" +); + +/** + * @internal + */ +export const parseContextual = Keyword.parse("contextual", "no-contextual"); + +/** + * @internal + */ +const parseLigature: Parser<Slice<Token>, List<Specified.Item>, string> = ( + input +) => { + let common: Specified.Common | undefined; + let discretionary: Specified.Discretionary | undefined; + let historical: Specified.Historical | undefined; + let contextual: Specified.Contextual | undefined; + + while (true) { + for (const [remainder] of Token.parseWhitespace(input)) { + input = remainder; + } + + if (common === undefined) { + const result = parseCommon(input); + + if (result.isOk()) { + [input, common] = result.get(); + continue; + } + } + + if (discretionary === undefined) { + const result = parseDiscretionary(input); + + if (result.isOk()) { + [input, discretionary] = result.get(); + continue; + } + } + + if (historical === undefined) { + const result = parseHistorical(input); + + if (result.isOk()) { + [input, historical] = result.get(); + continue; + } + } + + if (contextual === undefined) { + const result = parseContextual(input); + + if (result.isOk()) { + [input, contextual] = result.get(); + continue; + } + } + + break; + } + + if ( + common === undefined && + discretionary === undefined && + historical === undefined && + contextual === undefined + ) { + return Err.of("At least one ligature value must be provided"); + } + + return Result.of([ + input, + List.of( + [common, discretionary, historical, contextual].filter( + (value) => value !== undefined + // filter doesn't narrow so we need to do it manually + ) as Array<Specified.Item>, + " " + ), + ]); +}; + +/** + * @internal + */ +export const parse = either(Keyword.parse("none", "normal"), parseLigature); + +/** + * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/font-variant-ligatures} + * @internal + */ +export default Property.register( + "font-variant-ligatures", + Property.of<Specified, Computed>( + Keyword.of("normal"), + parse, + (ligatures) => ligatures, + { inherits: true } + ) +); diff --git a/packages/alfa-style/src/property/font-variant-numeric.ts b/packages/alfa-style/src/property/font-variant-numeric.ts new file mode 100644 index 0000000000..58f23c9e9e --- /dev/null +++ b/packages/alfa-style/src/property/font-variant-numeric.ts @@ -0,0 +1,168 @@ +import { Keyword, Token } from "@siteimprove/alfa-css"; +import { Parser } from "@siteimprove/alfa-parser"; +import { Err, Result } from "@siteimprove/alfa-result"; +import { Slice } from "@siteimprove/alfa-slice"; + +import { Property } from "../property"; +import { List } from "./value/list"; + +const { either } = Parser; + +declare module "../property" { + interface Longhands { + "font-variant-numeric": Property<Specified, Computed>; + } +} + +/** + * @internal + */ +export type Specified = Keyword<"normal"> | List<Specified.Item>; + +/** + * @internal + */ +export namespace Specified { + export type Figure = Keyword<"lining-nums"> | Keyword<"oldstyle-nums">; + + export type Spacing = Keyword<"proportional-nums"> | Keyword<"tabular-nums">; + + export type Fraction = + | Keyword<"diagonal-fractions"> + | Keyword<"stacked-fractions">; + + export type Item = + | Figure + | Spacing + | Fraction + | Keyword<"ordinal"> + | Keyword<"slashed-zero">; +} +/** + * @internal + */ +export type Computed = Specified; + +/** + * @internal + */ +export const parseFigure = Keyword.parse("lining-nums", "oldstyle-nums"); + +/** + * @internal + */ +export const parseSpacing = Keyword.parse("proportional-nums", "tabular-nums"); + +/** + * @internal + */ +export const parseFraction = Keyword.parse( + "diagonal-fractions", + "stacked-fractions" +); + +/** + * @internal + */ +const parseNumeric: Parser<Slice<Token>, List<Specified.Item>, string> = ( + input +) => { + let figure: Specified.Figure | undefined; + let spacing: Specified.Spacing | undefined; + let fraction: Specified.Fraction | undefined; + let ordinal: Keyword<"ordinal"> | undefined; + let slashed: Keyword<"slashed-zero"> | undefined; + + while (true) { + for (const [remainder] of Token.parseWhitespace(input)) { + input = remainder; + } + + if (figure === undefined) { + const result = parseFigure(input); + + if (result.isOk()) { + [input, figure] = result.get(); + continue; + } + } + + if (spacing === undefined) { + const result = parseSpacing(input); + + if (result.isOk()) { + [input, spacing] = result.get(); + continue; + } + } + + if (fraction === undefined) { + const result = parseFraction(input); + + if (result.isOk()) { + [input, fraction] = result.get(); + continue; + } + } + + if (ordinal === undefined) { + const result = Keyword.parse("ordinal")(input); + + if (result.isOk()) { + [input, ordinal] = result.get(); + continue; + } + } + + if (slashed === undefined) { + const result = Keyword.parse("slashed-zero")(input); + + if (result.isOk()) { + [input, slashed] = result.get(); + continue; + } + } + + break; + } + + if ( + figure === undefined && + spacing === undefined && + fraction === undefined && + ordinal === undefined && + slashed === undefined + ) { + return Err.of("At least one numeric value must be provided"); + } + + return Result.of([ + input, + List.of( + [figure, spacing, fraction, ordinal, slashed].filter( + (value) => value !== undefined + // filter doesn't narrow so we need to do it manually + ) as Array<Specified.Item>, + " " + ), + ]); +}; + +/** + * @internal + */ +export const parse = either(Keyword.parse("normal"), parseNumeric); + +/** + * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/font-variant-numeric} + * @internal + */ +export default Property.register( + "font-variant-numeric", + Property.of<Specified, Computed>( + Keyword.of("normal"), + parse, + (numeric) => numeric, + { inherits: true } + ) +); diff --git a/packages/alfa-style/src/property/font-variant-position.ts b/packages/alfa-style/src/property/font-variant-position.ts new file mode 100644 index 0000000000..f16bf08795 --- /dev/null +++ b/packages/alfa-style/src/property/font-variant-position.ts @@ -0,0 +1,38 @@ +import { Keyword } from "@siteimprove/alfa-css"; + +import { Property } from "../property"; + +declare module "../property" { + interface Longhands { + "font-variant-position": Property<Specified, Computed>; + } +} + +/** + * @internal + */ +export type Specified = Keyword<"normal"> | Keyword<"sub"> | Keyword<"super">; + +/** + * @internal + */ +export type Computed = Specified; + +/** + * @internal + */ +export const parse = Keyword.parse("normal", "sub", "super"); + +/** + * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/font-variant-position} + * @internal + */ +export default Property.register( + "font-variant-position", + Property.of<Specified, Computed>( + Keyword.of("normal"), + parse, + (position) => position, + { inherits: true } + ) +); diff --git a/packages/alfa-style/src/property/font-variant.ts b/packages/alfa-style/src/property/font-variant.ts new file mode 100644 index 0000000000..88fd6ff6da --- /dev/null +++ b/packages/alfa-style/src/property/font-variant.ts @@ -0,0 +1,262 @@ +import { Token, Keyword } from "@siteimprove/alfa-css"; +import { Parser } from "@siteimprove/alfa-parser"; +import { Err, Result } from "@siteimprove/alfa-result"; +import { Slice } from "@siteimprove/alfa-slice"; + +import { Property } from "../property"; + +import * as Caps from "./font-variant-caps"; +import * as EastAsian from "./font-variant-east-asian"; +import * as Ligatures from "./font-variant-ligatures"; +import * as Numeric from "./font-variant-numeric"; + +import { List } from "./value/list"; + +declare module "../property" { + interface Shorthands { + "font-variant": Property.Shorthand< + | "font-variant-caps" + | "font-variant-east-asian" + | "font-variant-ligatures" + | "font-variant-numeric" + >; + } +} + +/** + * @internal + */ +export const parse: Parser< + Slice<Token>, + [ + ["font-variant-caps", Caps.Specified | Keyword<"initial">], + ["font-variant-east-asian", EastAsian.Specified | Keyword<"initial">], + ["font-variant-ligatures", Ligatures.Specified | Keyword<"initial">], + ["font-variant-numeric", Numeric.Specified | Keyword<"initial">] + ], + string +> = (input) => { + /* Unfortunately, the various components of each longhand can be mixed, so + * we need to rewrite a parser and accept, e.g. + * font-variant: historical-ligatures diagonal-fractions no-common-ligatures ordinal + */ + let caps: Caps.Specified | undefined; + + let variant: EastAsian.Specified.Variant | undefined; + let width: EastAsian.Specified.Width | undefined; + let ruby: Keyword<"ruby"> | undefined; + + let common: Ligatures.Specified.Common | undefined; + let discretionary: Ligatures.Specified.Discretionary | undefined; + let historical: Ligatures.Specified.Historical | undefined; + let contextual: Ligatures.Specified.Contextual | undefined; + + let figure: Numeric.Specified.Figure | undefined; + let spacing: Numeric.Specified.Spacing | undefined; + let fraction: Numeric.Specified.Fraction | undefined; + let ordinal: Keyword<"ordinal"> | undefined; + let slashed: Keyword<"slashed-zero"> | undefined; + + while (true) { + for (const [remainder] of Token.parseWhitespace(input)) { + input = remainder; + } + + // ------------------------- Caps + if (caps === undefined) { + const result = Caps.parse(input); + + if (result.isOk()) { + [input, caps] = result.get(); + continue; + } + } + + // ------------------------- East Asian + if (variant === undefined) { + const result = EastAsian.parseVariant(input); + + if (result.isOk()) { + [input, variant] = result.get(); + continue; + } + } + + if (width === undefined) { + const result = EastAsian.parseWidth(input); + + if (result.isOk()) { + [input, width] = result.get(); + continue; + } + } + + if (ruby === undefined) { + const result = Keyword.parse("ruby")(input); + + if (result.isOk()) { + [input, ruby] = result.get(); + continue; + } + } + + // ------------------------- Ligatures + if (common === undefined) { + const result = Ligatures.parseCommon(input); + + if (result.isOk()) { + [input, common] = result.get(); + continue; + } + } + + if (discretionary === undefined) { + const result = Ligatures.parseDiscretionary(input); + + if (result.isOk()) { + [input, discretionary] = result.get(); + continue; + } + } + + if (historical === undefined) { + const result = Ligatures.parseHistorical(input); + + if (result.isOk()) { + [input, historical] = result.get(); + continue; + } + } + + if (contextual === undefined) { + const result = Ligatures.parseContextual(input); + + if (result.isOk()) { + [input, contextual] = result.get(); + continue; + } + } + + // ------------------------- Numeric + if (figure === undefined) { + const result = Numeric.parseFigure(input); + + if (result.isOk()) { + [input, figure] = result.get(); + continue; + } + } + + if (spacing === undefined) { + const result = Numeric.parseSpacing(input); + + if (result.isOk()) { + [input, spacing] = result.get(); + continue; + } + } + + if (fraction === undefined) { + const result = Numeric.parseFraction(input); + + if (result.isOk()) { + [input, fraction] = result.get(); + continue; + } + } + + if (ordinal === undefined) { + const result = Keyword.parse("ordinal")(input); + + if (result.isOk()) { + [input, ordinal] = result.get(); + continue; + } + } + + if (slashed === undefined) { + const result = Keyword.parse("slashed-zero")(input); + + if (result.isOk()) { + [input, slashed] = result.get(); + continue; + } + } + + break; + } + + if ( + caps === undefined && + variant === undefined && + width === undefined && + ruby === undefined && + common === undefined && + discretionary === undefined && + historical === undefined && + contextual === undefined && + figure === undefined && + spacing === undefined && + fraction === undefined && + ordinal === undefined && + slashed === undefined + ) { + return Err.of("At least one Font variant value must be provided"); + } + + function list<T>( + ...values: Array<T | undefined> + ): List<T> | Keyword<"initial"> { + // filter doesn't narrow so we need to do it manually + const cleaned = values.filter((value) => value !== undefined) as Array<T>; + + return cleaned.length > 0 ? List.of(cleaned, " ") : Keyword.of("initial"); + } + + return Result.of([ + input, + [ + ["font-variant-caps", caps ?? Keyword.of("initial")], + [ + "font-variant-east-asian", + list<EastAsian.Specified.Item>(variant, width, ruby), + ], + [ + "font-variant-ligatures", + list<Ligatures.Specified.Item>( + common, + discretionary, + historical, + contextual + ), + ], + [ + "font-variant-numeric", + list<Numeric.Specified.Item>( + figure, + spacing, + fraction, + ordinal, + slashed + ), + ], + ], + ]); +}; + +/** + * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/font-variant} + * @internal + */ +export default Property.registerShorthand( + "font-variant", + Property.shorthand( + [ + "font-variant-caps", + "font-variant-east-asian", + "font-variant-ligatures", + "font-variant-numeric", + ], + parse + ) +); diff --git a/packages/alfa-style/src/property/font.ts b/packages/alfa-style/src/property/font.ts index 102c955559..ac6f5d23d3 100644 --- a/packages/alfa-style/src/property/font.ts +++ b/packages/alfa-style/src/property/font.ts @@ -14,6 +14,8 @@ import * as Weight from "./font-weight"; const { map, option, pair, right, delimited } = Parser; +// font may only set font-variant-caps to small-caps, but setting font +// does reset all font-variant-* longhand to initial value (this is good!) declare module "../property" { interface Shorthands { font: Property.Shorthand< @@ -21,6 +23,11 @@ declare module "../property" { | "font-size" | "font-stretch" | "font-style" + | "font-variant-caps" + | "font-variant-east-asian" + | "font-variant-ligatures" + | "font-variant-numeric" + | "font-variant-position" | "font-weight" | "line-height" >; @@ -35,6 +42,11 @@ export const parsePrelude: Parser< [ ["font-stretch", Stretch.Specified | Keyword<"initial">], ["font-style", Style.Specified | Keyword<"initial">], + // only "normal" and "small-caps" are accepted in font… + [ + "font-variant-caps", + Keyword<"normal"> | Keyword<"small-caps"> | Keyword<"initial"> + ], ["font-weight", Weight.Specified | Keyword<"initial">] ], string @@ -93,6 +105,7 @@ export const parsePrelude: Parser< [ ["font-stretch", stretch ?? Keyword.of("initial")], ["font-style", style ?? Keyword.of("initial")], + ["font-variant-caps", variant ?? Keyword.of("initial")], ["font-weight", weight ?? Keyword.of("initial")], ], ]); @@ -129,6 +142,11 @@ export default Property.registerShorthand( "font-size", "font-stretch", "font-style", + "font-variant-caps", + "font-variant-east-asian", + "font-variant-ligatures", + "font-variant-numeric", + "font-variant-position", "font-weight", "line-height", ], diff --git a/packages/alfa-style/src/property/text-decoration-thickness.ts b/packages/alfa-style/src/property/text-decoration-thickness.ts new file mode 100644 index 0000000000..0564f34681 --- /dev/null +++ b/packages/alfa-style/src/property/text-decoration-thickness.ts @@ -0,0 +1,63 @@ +import { Keyword, Length, Percentage, Token } from "@siteimprove/alfa-css"; +import { Parser } from "@siteimprove/alfa-parser"; +import { Slice } from "@siteimprove/alfa-slice"; + +import { Property } from "../property"; +import { Resolver } from "../resolver"; + +const { either } = Parser; + +declare module "../property" { + interface Longhands { + "text-decoration-thickness": Property<Specified, Computed>; + } +} + +/** + * @internal + */ +export type Specified = + | Length + | Percentage + | Keyword<"auto"> + | Keyword<"from-font">; + +/** + * @internal + */ +export type Computed = Length<"px"> | Keyword<"auto"> | Keyword<"from-font">; + +/** + * @internal + */ +export const parse = either<Slice<Token>, Specified, string>( + Keyword.parse("auto", "from-font"), + Length.parse, + Percentage.parse +); + +/** + * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/text-decoration-thickness} + * @internal + */ +export default Property.register( + "text-decoration-thickness", + Property.of<Specified, Computed>( + Keyword.of("auto"), + parse, + (thickness, style) => + thickness.map((value) => { + switch (value.type) { + case "keyword": + return value; + case "length": + return Resolver.length(value, style); + case "percentage": + return Length.of( + style.computed("font-size").value.value * value.value, + "px" + ); + } + }) + ) +); diff --git a/packages/alfa-style/src/property/text-decoration.ts b/packages/alfa-style/src/property/text-decoration.ts index e9f4051ffc..878f492179 100644 --- a/packages/alfa-style/src/property/text-decoration.ts +++ b/packages/alfa-style/src/property/text-decoration.ts @@ -6,11 +6,15 @@ import { Property } from "../property"; import * as Color from "./text-decoration-color"; import * as Line from "./text-decoration-line"; import * as Style from "./text-decoration-style"; +import * as Thickness from "./text-decoration-thickness"; declare module "../property" { interface Shorthands { "text-decoration": Property.Shorthand< - "text-decoration-line" | "text-decoration-style" | "text-decoration-color" + | "text-decoration-line" + | "text-decoration-style" + | "text-decoration-color" + | "text-decoration-thickness" >; } } @@ -22,11 +26,17 @@ declare module "../property" { export default Property.registerShorthand( "text-decoration", Property.shorthand( - ["text-decoration-line", "text-decoration-style", "text-decoration-color"], + [ + "text-decoration-line", + "text-decoration-style", + "text-decoration-color", + "text-decoration-thickness", + ], (input) => { let line: Line.Specified | undefined; let style: Style.Specified | undefined; let color: Color.Specified | undefined; + let thickness: Thickness.Specified | undefined; while (true) { for (const [remainder] of Token.parseWhitespace(input)) { @@ -60,11 +70,25 @@ export default Property.registerShorthand( } } + if (thickness === undefined) { + const result = Thickness.parse(input); + + if (result.isOk()) { + [input, thickness] = result.get(); + continue; + } + } + break; } - if (line === undefined && style === undefined && color === undefined) { - return Err.of(`Expected one of line, style, or color`); + if ( + line === undefined && + style === undefined && + color === undefined && + thickness === undefined + ) { + return Err.of(`Expected one of line, style, color, or thickness`); } return Result.of([ @@ -73,6 +97,7 @@ export default Property.registerShorthand( ["text-decoration-line", line ?? Keyword.of("initial")], ["text-decoration-style", style ?? Keyword.of("initial")], ["text-decoration-color", color ?? Keyword.of("initial")], + ["text-decoration-thickness", thickness ?? Keyword.of("initial")], ], ]); } diff --git a/packages/alfa-style/test/property/background-size.spec.tsx b/packages/alfa-style/test/property/background-size.spec.tsx new file mode 100644 index 0000000000..5920772501 --- /dev/null +++ b/packages/alfa-style/test/property/background-size.spec.tsx @@ -0,0 +1,160 @@ +import { Device } from "@siteimprove/alfa-device"; +import { h } from "@siteimprove/alfa-dom"; +import { test } from "@siteimprove/alfa-test"; +import { Style } from "../../src"; + +const device = Device.standard(); + +test("#cascaded() parses `background-size: cover`", (t) => { + const element = <div style={{ backgroundSize: `cover` }} />; + + const style = Style.from(element, device); + + t.deepEqual(style.cascaded("background-size").get().toJSON(), { + value: { + type: "list", + values: [ + { + type: "keyword", + value: "cover", + }, + ], + separator: ", ", + }, + source: h.declaration("background-size", "cover").toJSON(), + }); +}); + +test("#cascaded() parses `background-size: 10px`", (t) => { + const element = <div style={{ backgroundSize: `10px` }} />; + + const style = Style.from(element, device); + + t.deepEqual(style.cascaded("background-size").get().toJSON(), { + value: { + type: "list", + values: [ + { + type: "tuple", + values: [ + { + type: "length", + value: 10, + unit: "px", + }, + { + type: "keyword", + value: "auto", + }, + ], + }, + ], + separator: ", ", + }, + source: h.declaration("background-size", "10px").toJSON(), + }); +}); + +test("#cascaded() parses `background-size: 10%`", (t) => { + const element = <div style={{ backgroundSize: `10%` }} />; + + const style = Style.from(element, device); + + t.deepEqual(style.cascaded("background-size").get().toJSON(), { + value: { + type: "list", + values: [ + { + type: "tuple", + values: [ + { + type: "percentage", + value: 0.1, + }, + { + type: "keyword", + value: "auto", + }, + ], + }, + ], + separator: ", ", + }, + source: h.declaration("background-size", "10%").toJSON(), + }); +}); + +test("#cascaded() parses `background-size: 10px 20px`", (t) => { + const element = <div style={{ backgroundSize: `10px 20px` }} />; + + const style = Style.from(element, device); + + t.deepEqual(style.cascaded("background-size").get().toJSON(), { + value: { + type: "list", + values: [ + { + type: "tuple", + values: [ + { + type: "length", + value: 10, + unit: "px", + }, + { + type: "length", + value: 20, + unit: "px", + }, + ], + }, + ], + separator: ", ", + }, + source: h.declaration("background-size", "10px 20px").toJSON(), + }); +}); + +test("#cascaded() parses `background-size: 10px, 20px`", (t) => { + const element = <div style={{ backgroundSize: `10px, 20px` }} />; + + const style = Style.from(element, device); + + t.deepEqual(style.cascaded("background-size").get().toJSON(), { + value: { + type: "list", + values: [ + { + type: "tuple", + values: [ + { + type: "length", + value: 10, + unit: "px", + }, + { + type: "keyword", + value: "auto", + }, + ], + }, + { + type: "tuple", + values: [ + { + type: "length", + value: 20, + unit: "px", + }, + { + type: "keyword", + value: "auto", + }, + ], + }, + ], + separator: ", ", + }, + source: h.declaration("background-size", "10px, 20px").toJSON(), + }); +}); diff --git a/packages/alfa-style/test/property/font-variant.spec.tsx b/packages/alfa-style/test/property/font-variant.spec.tsx new file mode 100644 index 0000000000..b98c5817c6 --- /dev/null +++ b/packages/alfa-style/test/property/font-variant.spec.tsx @@ -0,0 +1,52 @@ +import { test } from "@siteimprove/alfa-test"; + +import { Device } from "@siteimprove/alfa-device"; + +import { Style } from "../../src/style"; + +const device = Device.standard(); + +test(`#cascaded() parses \`font-variant: oldstyle-nums ruby + historical-ligatures diagonal-fractions ordinal contextual slashed-zero\``, (t) => { + const element = ( + <div + style={{ + "font-variant": + "oldstyle-nums ruby historical-ligatures diagonal-fractions ordinal contextual slashed-zero", + }} + /> + ); + + const style = Style.from(element, device); + + t.deepEqual(style.cascaded("font-variant-caps").get().value.toJSON(), { + type: "keyword", + value: "initial", + }); + + t.deepEqual(style.cascaded("font-variant-east-asian").get().value.toJSON(), { + type: "list", + values: [{ type: "keyword", value: "ruby" }], + separator: " ", + }); + + t.deepEqual(style.cascaded("font-variant-ligatures").get().value.toJSON(), { + type: "list", + values: [ + { type: "keyword", value: "historical-ligatures" }, + { type: "keyword", value: "contextual" }, + ], + separator: " ", + }); + + t.deepEqual(style.cascaded("font-variant-numeric").get().value.toJSON(), { + type: "list", + values: [ + { type: "keyword", value: "oldstyle-nums" }, + { type: "keyword", value: "diagonal-fractions" }, + { type: "keyword", value: "ordinal" }, + { type: "keyword", value: "slashed-zero" }, + ], + separator: " ", + }); +}); diff --git a/packages/alfa-style/test/property/text-decoration.spec.tsx b/packages/alfa-style/test/property/text-decoration.spec.tsx index dc7e0db954..589bc023e3 100644 --- a/packages/alfa-style/test/property/text-decoration.spec.tsx +++ b/packages/alfa-style/test/property/text-decoration.spec.tsx @@ -160,3 +160,54 @@ test("#cascaded() parses `text-decoration: underline solid red`", (t) => { source: h.declaration("text-decoration", "underline solid red").toJSON(), }); }); + +test("#cascaded() parses `text-decoration: underline solid red 2px`", (t) => { + const element = <div style={{ textDecoration: "underline solid red 2px" }} />; + + const declaration = h.declaration( + "text-decoration", + "underline solid red 2px" + ); + + const style = Style.from(element, device); + + t.deepEqual(style.cascaded("text-decoration-line").get().toJSON(), { + value: { + type: "list", + values: [ + { + type: "keyword", + value: "underline", + }, + ], + separator: " ", + }, + source: declaration.toJSON(), + }); + + t.deepEqual(style.cascaded("text-decoration-style").get().toJSON(), { + value: { + type: "keyword", + value: "solid", + }, + source: declaration.toJSON(), + }); + + t.deepEqual(style.cascaded("text-decoration-color").get().toJSON(), { + value: { + type: "color", + format: "named", + color: "red", + }, + source: declaration.toJSON(), + }); + + t.deepEqual(style.cascaded("text-decoration-thickness").get().toJSON(), { + value: { + type: "length", + value: 2, + unit: "px", + }, + source: declaration.toJSON(), + }); +}); diff --git a/packages/alfa-style/tsconfig.json b/packages/alfa-style/tsconfig.json index dcc9cccbda..6b7bddb5c0 100644 --- a/packages/alfa-style/tsconfig.json +++ b/packages/alfa-style/tsconfig.json @@ -92,6 +92,12 @@ "src/property/font-size.ts", "src/property/font-stretch.ts", "src/property/font-style.ts", + "src/property/font-variant.ts", + "src/property/font-variant-caps.ts", + "src/property/font-variant-east-asian.ts", + "src/property/font-variant-ligatures.ts", + "src/property/font-variant-numeric.ts", + "src/property/font-variant-position.ts", "src/property/font-weight.ts", "src/property/height.ts", "src/property/inset.ts", @@ -120,6 +126,7 @@ "src/property/text-decoration-color.ts", "src/property/text-decoration-line.ts", "src/property/text-decoration-style.ts", + "src/property/text-decoration-thickness.ts", "src/property/text-indent.ts", "src/property/text-overflow.ts", "src/property/text-transform.ts", @@ -138,6 +145,7 @@ "test/property/background-color.spec.tsx", "test/property/background-image.spec.tsx", "test/property/background-position.spec.tsx", + "test/property/background-size.spec.tsx", "test/property/border.spec.tsx", "test/property/border-[block,inline].spec.tsx", "test/property/border-[block,inline]-[end,start].spec.tsx", @@ -163,6 +171,7 @@ "test/property/font.spec.tsx", "test/property/font-family.spec.tsx", "test/property/font-size.spec.tsx", + "test/property/font-variant.spec.tsx", "test/property/inset-block.spec.tsx", "test/property/text-decoration.spec.tsx", "test/property/text-indent.spec.tsx",