From a2c09e051bee53947899cca3631f2ee514025330 Mon Sep 17 00:00:00 2001 From: Jean-Yves Moyen Date: Tue, 20 Apr 2021 16:36:19 +0200 Subject: [PATCH 01/46] Add basic tests for R11 (#770) --- .../alfa-rules/test/sia-r11/rule.spec.tsx | 47 +++++++++++++++++++ packages/alfa-rules/tsconfig.json | 1 + 2 files changed, 48 insertions(+) create mode 100644 packages/alfa-rules/test/sia-r11/rule.spec.tsx 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..ce148fd6b4 --- /dev/null +++ b/packages/alfa-rules/test/sia-r11/rule.spec.tsx @@ -0,0 +1,47 @@ +import { test } from "@siteimprove/alfa-test"; +import { Document } from "@siteimprove/alfa-dom"; + +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 = Foo; + + const document = Document.of([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 = ; + + const document = Document.of([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 = Document.of([
]); + + 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 = ( + + Foo + + ); + + const document = Document.of([target]); + + t.deepEqual(await evaluate(R11, { document }), [ + passed(R11, target, { 1: Outcomes.HasName }), + ]); +}); diff --git a/packages/alfa-rules/tsconfig.json b/packages/alfa-rules/tsconfig.json index 34badac221..cf800bb7ea 100644 --- a/packages/alfa-rules/tsconfig.json +++ b/packages/alfa-rules/tsconfig.json @@ -157,6 +157,7 @@ "test/sia-r7/rule.spec.tsx", "test/sia-r9/rule.spec.tsx", "test/sia-r10/rule.spec.tsx", + "test/sia-r11/rule.spec.tsx", "test/sia-r13/rule.spec.tsx", "test/sia-r14/rule.spec.tsx", "test/sia-r15/rule.spec.tsx", From b3b3d062fa05c913cdb8eacea9bd571513aa1083 Mon Sep 17 00:00:00 2001 From: Jean-Yves Moyen Date: Wed, 21 Apr 2021 10:04:09 +0200 Subject: [PATCH 02/46] Remove second expectation from R62 (#772) --- packages/alfa-rules/src/sia-r62/rule.ts | 34 ------------------- .../alfa-rules/test/sia-r62/rule.spec.tsx | 17 ---------- 2 files changed, 51 deletions(-) diff --git a/packages/alfa-rules/src/sia-r62/rule.ts b/packages/alfa-rules/src/sia-r62/rule.ts index f79f4acc36..0a5606ef70 100644 --- a/packages/alfa-rules/src/sia-r62/rule.ts +++ b/packages/alfa-rules/src/sia-r62/rule.ts @@ -94,27 +94,6 @@ export default Rule.Atomic.of({ () => 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 - ), }; }, }; @@ -132,19 +111,6 @@ export namespace Outcomes { 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` - ) - ); } function hasNonLinkText(device: Device): Predicate { diff --git a/packages/alfa-rules/test/sia-r62/rule.spec.tsx b/packages/alfa-rules/test/sia-r62/rule.spec.tsx index b0ec286f55..b43164bbe9 100644 --- a/packages/alfa-rules/test/sia-r62/rule.spec.tsx +++ b/packages/alfa-rules/test/sia-r62/rule.spec.tsx @@ -17,7 +17,6 @@ test(`evaluate() passes an element with a

parent element with non-link t.deepEqual(await evaluate(R62, { document }), [ passed(R62, target, { 1: Outcomes.IsDistinguishable, - 2: Outcomes.IsDistinguishableWhenVisited, }), ]); }); @@ -35,7 +34,6 @@ test(`evaluate() passes an element with a

parent element with non-link t.deepEqual(await evaluate(R62, { document }), [ passed(R62, target, { 1: Outcomes.IsDistinguishable, - 2: Outcomes.IsDistinguishableWhenVisited, }), ]); }); @@ -58,7 +56,6 @@ test(`evaluate() fails an element that removes the default text decoration t.deepEqual(await evaluate(R62, { document }), [ failed(R62, target, { 1: Outcomes.IsNotDistinguishable, - 2: Outcomes.IsNotDistinguishableWhenVisited, }), ]); }); @@ -81,7 +78,6 @@ test(`evaluate() fails an element that removes the default text decoration t.deepEqual(await evaluate(R62, { document }), [ failed(R62, target, { 1: Outcomes.IsNotDistinguishable, - 2: Outcomes.IsNotDistinguishableWhenVisited, }), ]); }); @@ -106,7 +102,6 @@ test(`evaluate() fails an element that removes the default text decoration t.deepEqual(await evaluate(R62, { document }), [ failed(R62, target, { 1: Outcomes.IsNotDistinguishable, - 2: Outcomes.IsNotDistinguishableWhenVisited, }), ]); }); @@ -134,7 +129,6 @@ test(`evaluate() fails an element that removes the default text decoration t.deepEqual(await evaluate(R62, { document }), [ failed(R62, target, { 1: Outcomes.IsNotDistinguishable, - 2: Outcomes.IsNotDistinguishableWhenVisited, }), ]); }); @@ -161,7 +155,6 @@ test(`evaluate() fails an element that applies a text decoration only on t.deepEqual(await evaluate(R62, { document }), [ failed(R62, target, { 1: Outcomes.IsNotDistinguishable, - 2: Outcomes.IsNotDistinguishableWhenVisited, }), ]); }); @@ -188,7 +181,6 @@ test(`evaluate() fails an element that applies a text decoration only on t.deepEqual(await evaluate(R62, { document }), [ failed(R62, target, { 1: Outcomes.IsNotDistinguishable, - 2: Outcomes.IsNotDistinguishableWhenVisited, }), ]); }); @@ -215,7 +207,6 @@ test(`evaluate() fails an element that applies a text decoration only on t.deepEqual(await evaluate(R62, { document }), [ failed(R62, target, { 1: Outcomes.IsNotDistinguishable, - 2: Outcomes.IsNotDistinguishableWhenVisited, }), ]); }); @@ -239,7 +230,6 @@ test(`evaluate() passes an applicable element that removes the default text t.deepEqual(await evaluate(R62, { document }), [ passed(R62, target, { 1: Outcomes.IsDistinguishable, - 2: Outcomes.IsDistinguishableWhenVisited, }), ]); }); @@ -263,7 +253,6 @@ test(`evaluate() passes an applicable element that removes the default text t.deepEqual(await evaluate(R62, { document }), [ passed(R62, target, { 1: Outcomes.IsDistinguishable, - 2: Outcomes.IsDistinguishableWhenVisited, }), ]); }); @@ -287,7 +276,6 @@ test(`evaluate() fails an element that has no distinguishing features and t.deepEqual(await evaluate(R62, { document }), [ failed(R62, target, { 1: Outcomes.IsNotDistinguishable, - 2: Outcomes.IsNotDistinguishableWhenVisited, }), ]); }); @@ -311,7 +299,6 @@ test(`evaluate() fails an element that has no distinguishing features and t.deepEqual(await evaluate(R62, { document }), [ failed(R62, target, { 1: Outcomes.IsNotDistinguishable, - 2: Outcomes.IsNotDistinguishableWhenVisited, }), ]); }); @@ -335,7 +322,6 @@ test(`evaluate() passes an applicable element that removes the default text t.deepEqual(await evaluate(R62, { document }), [ passed(R62, target, { 1: Outcomes.IsDistinguishable, - 2: Outcomes.IsDistinguishableWhenVisited, }), ]); }); @@ -362,7 +348,6 @@ test(`evaluate() fails an element that has no distinguishing features but is t.deepEqual(await evaluate(R62, { document }), [ failed(R62, target, { 1: Outcomes.IsNotDistinguishable, - 2: Outcomes.IsNotDistinguishableWhenVisited, }), ]); }); @@ -390,7 +375,6 @@ test(`evaluate() fails an element that has no distinguishing features and t.deepEqual(await evaluate(R62, { document }), [ failed(R62, target, { 1: Outcomes.IsNotDistinguishable, - 2: Outcomes.IsNotDistinguishableWhenVisited, }), ]); }); @@ -442,7 +426,6 @@ test(`evaluate() passes an element with a

parent elem t.deepEqual(await evaluate(R62, { document }), [ passed(R62, target, { 1: Outcomes.IsDistinguishable, - 2: Outcomes.IsDistinguishableWhenVisited, }), ]); }); From 53089a66c92da5ff40919869d9c36310c14a5ea0 Mon Sep 17 00:00:00 2001 From: Jean-Yves Moyen Date: Wed, 21 Apr 2021 10:28:43 +0200 Subject: [PATCH 03/46] Implement pseudo classes equality (#773) * Add toString() to Nth * Add equals() and toString() to functional pseudo-classes and elements --- packages/alfa-css/src/syntax/nth.ts | 6 +++ packages/alfa-selector/src/selector.ts | 68 ++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) 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-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 { From 6aac6f4e3251e854383669964ab1761df3055e31 Mon Sep 17 00:00:00 2001 From: Jean-Yves Moyen Date: Tue, 27 Apr 2021 14:50:34 +0200 Subject: [PATCH 04/46] Name computation: skip step 1 when descending (#778) * Add failing example * Skip step 1 when descending --- packages/alfa-aria/src/name.ts | 8 ++- packages/alfa-aria/test/name.spec.tsx | 80 +++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/packages/alfa-aria/src/name.ts b/packages/alfa-aria/src/name.ts index 0c5bec8d55..10bd83c3dd 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; } diff --git a/packages/alfa-aria/test/name.spec.tsx b/packages/alfa-aria/test/name.spec.tsx index 4cd88c9bf0..a826b00356 100644 --- a/packages/alfa-aria/test/name.spec.tsx +++ b/packages/alfa-aria/test/name.spec.tsx @@ -473,6 +473,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 = ( From e6fb3bbdcc84e96c98c359a51ac3264ca618b55f Mon Sep 17 00:00:00 2001 From: Jean-Yves Moyen Date: Thu, 29 Apr 2021 11:13:02 +0200 Subject: [PATCH 05/46] SIA R62: accept different font weight (#779) * Add failing example * Accept font weight difference with container as good for SIA-R62 --- packages/alfa-rules/src/sia-r62/rule.ts | 24 +++++++++++++++++++ .../alfa-rules/test/sia-r62/rule.spec.tsx | 24 +++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/packages/alfa-rules/src/sia-r62/rule.ts b/packages/alfa-rules/src/sia-r62/rule.ts index 0a5606ef70..1c00bea762 100644 --- a/packages/alfa-rules/src/sia-r62/rule.ts +++ b/packages/alfa-rules/src/sia-r62/rule.ts @@ -142,6 +142,8 @@ function isDistinguishable( 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 @@ -189,3 +191,25 @@ 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)); + }; +} diff --git a/packages/alfa-rules/test/sia-r62/rule.spec.tsx b/packages/alfa-rules/test/sia-r62/rule.spec.tsx index b43164bbe9..bdc8acc9b8 100644 --- a/packages/alfa-rules/test/sia-r62/rule.spec.tsx +++ b/packages/alfa-rules/test/sia-r62/rule.spec.tsx @@ -442,3 +442,27 @@ test(`evaluate() is inapplicable to an element with a

parent element t.deepEqual(await evaluate(R62, { document }), [inapplicable(R62)]); }); + +test(`evaluate() passes a link whose bolder than surrounding text`, async (t) => { + const target = Link; + + const document = Document.of( + [ +

+ Hello {target} +

, + ], + [ + h.sheet([ + h.rule.style("a", { + textDecoration: "none", + fontWeight: "bold", + }), + ]), + ] + ); + + t.deepEqual(await evaluate(R62, { document }), [ + passed(R62, target, { 1: Outcomes.IsDistinguishable }), + ]); +}); From 39be660e6d87835c2eb257d9df24b1d071df496e Mon Sep 17 00:00:00 2001 From: Jean-Yves Moyen Date: Tue, 4 May 2021 11:16:59 +0200 Subject: [PATCH 06/46] Extended diagnostic for R14 (#786) * Add label and name in Diagnostic for R14 * Add tests for R42 --- packages/alfa-rules/src/sia-r14/rule.ts | 93 +++++++++-- .../alfa-rules/test/sia-r14/rule.spec.tsx | 8 +- .../alfa-rules/test/sia-r42/rule.spec.tsx | 145 ++++++++++++++++++ packages/alfa-rules/tsconfig.json | 1 + 4 files changed, 228 insertions(+), 19 deletions(-) create mode 100644 packages/alfa-rules/test/sia-r42/rule.spec.tsx diff --git a/packages/alfa-rules/src/sia-r14/rule.ts b/packages/alfa-rules/src/sia-r14/rule.ts index 18463bc29e..7b173e833f 100644 --- a/packages/alfa-rules/src/sia-r14/rule.ts +++ b/packages/alfa-rules/src/sia-r14/rule.ts @@ -47,19 +47,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) ), }; }, @@ -83,15 +85,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/test/sia-r14/rule.spec.tsx b/packages/alfa-rules/test/sia-r14/rule.spec.tsx index 3fc87ad380..cd3e04e474 100644 --- a/packages/alfa-rules/test/sia-r14/rule.spec.tsx +++ b/packages/alfa-rules/test/sia-r14/rule.spec.tsx @@ -15,7 +15,7 @@ test(`evaluate() passes a +
; + + const document = Document.of([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 = ; + + const document = Document.of([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 = ; + + const document = Document.of([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 =; + + const document = Document.of([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 =; + + const document = Document.of([target]); + + t.deepEqual(await evaluate(R17, { document }), [ + failed(R17, target, { + 1: Outcomes.IsTabbable, + }), + ]); + }); + + test(`evaluate() fails a focusable summary element`, async (t) => { + const target = ; + + const document = Document.of([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 =
+

Some text

+
; + + const document = Document.of([target]); + + t.deepEqual(await evaluate(R17, { document }), [inapplicable(R17)]); + }); + + + diff --git a/packages/alfa-rules/tsconfig.json b/packages/alfa-rules/tsconfig.json index 1d64078a3c..6c06ca7fed 100644 --- a/packages/alfa-rules/tsconfig.json +++ b/packages/alfa-rules/tsconfig.json @@ -162,6 +162,7 @@ "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-r21/rule.spec.tsx", "test/sia-r24/rule.spec.tsx", "test/sia-r38/rule.spec.tsx", From a78c5277983b3b7170f61e750fc285dc710c65aa Mon Sep 17 00:00:00 2001 From: elenamongelli <52746406+elenamongelli@users.noreply.github.com> Date: Fri, 7 May 2021 12:17:40 +0200 Subject: [PATCH 10/46] SIA R4: test added (#793) --- packages/alfa-rules/test/sia-r4/rule.spec.tsx | 64 +++++++++++++++++++ packages/alfa-rules/tsconfig.json | 1 + 2 files changed, 65 insertions(+) create mode 100644 packages/alfa-rules/test/sia-r4/rule.spec.tsx 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..9ebf2a153a --- /dev/null +++ b/packages/alfa-rules/test/sia-r4/rule.spec.tsx @@ -0,0 +1,64 @@ +import { test } from "@siteimprove/alfa-test"; + +import { Document } from "@siteimprove/alfa-dom"; + +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 = ; + + const document = Document.of([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 = ; + + const document = Document.of([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 = ; + + const document = Document.of([target]); + + t.deepEqual(await evaluate(R4, { document }), [ + failed(R4, target, { + 1: Outcomes.HasNoLanguage, + }), + ]); + }); + +test(`evaluate() fails an html element with xml:lang attribute.`, async (t) => { + const target = ; + + const document = Document.of([target]); + + t.deepEqual(await evaluate(R4, { document }), [ + failed(R4, target, { + 1: Outcomes.HasNoLanguage, + }), + ]); + }); + + test(`evaluate() is inapplicable to svg element.`, async (t) => { + const target = ; + + const document = Document.of([target]); + + t.deepEqual(await evaluate(R4, { document }), [inapplicable(R4)]); + }); \ No newline at end of file diff --git a/packages/alfa-rules/tsconfig.json b/packages/alfa-rules/tsconfig.json index 6c06ca7fed..597c0ce17c 100644 --- a/packages/alfa-rules/tsconfig.json +++ b/packages/alfa-rules/tsconfig.json @@ -154,6 +154,7 @@ "test/common/expectation/get-colors.spec.tsx", "test/sia-r1/rule.spec.tsx", "test/sia-r2/rule.spec.tsx", + "test/sia-r4/rule.spec.tsx", "test/sia-r7/rule.spec.tsx", "test/sia-r9/rule.spec.tsx", "test/sia-r10/rule.spec.tsx", From 69ea3cd25ddaa83b6ccdbcea8bc7cbf30afb677f Mon Sep 17 00:00:00 2001 From: elenamongelli <52746406+elenamongelli@users.noreply.github.com> Date: Fri, 7 May 2021 12:58:00 +0200 Subject: [PATCH 11/46] SIA R3: add test (#792) * Sia r3 testing added * Apply suggestions from code review Co-authored-by: Jean-Yves Moyen Co-authored-by: Jean-Yves Moyen --- packages/alfa-rules/test/sia-r3/rule.spec.tsx | 84 +++++++++++++++++++ packages/alfa-rules/tsconfig.json | 1 + 2 files changed, 85 insertions(+) create mode 100644 packages/alfa-rules/test/sia-r3/rule.spec.tsx 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..0adcf1803f --- /dev/null +++ b/packages/alfa-rules/test/sia-r3/rule.spec.tsx @@ -0,0 +1,84 @@ +import { test } from "@siteimprove/alfa-test"; + +import { Document } from "@siteimprove/alfa-dom"; + +import R3, { Outcomes } from "../../src/sia-r3/rule"; + +import { evaluate } from "../common/evaluate"; +import { passed, failed, inapplicable } from "../common/outcome"; + +test("evaluate() passes one id attribute in the document", async (t) => { + const target =
This is my first element
; + + const document = Document.of([target]); + + t.deepEqual(await evaluate(R3, { document }), [ + passed(R3, target, { + 1: Outcomes.HasUniqueId, + }), + ]); + }); + +test("evaluate() passes unique id attributes in the document context.", async (t) => { + const target1 =
This is my first element
; + const target2 =
This is my second element
; + const target3 = This is my third element; + + const document = Document.of([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 in the document context.", async (t) => { + const target1 =
Name
; + const target2 =
City
; + + const document = Document.of([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 =
Name
; + const target2 = + City + ; + + const document = Document.of([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", async (t) => { + const target1 =
This is my first element
; + + const document = Document.of([target1]); + + t.deepEqual(await evaluate(R3, { document }), [inapplicable(R3)]); + }); + + diff --git a/packages/alfa-rules/tsconfig.json b/packages/alfa-rules/tsconfig.json index 597c0ce17c..c3c141edb8 100644 --- a/packages/alfa-rules/tsconfig.json +++ b/packages/alfa-rules/tsconfig.json @@ -154,6 +154,7 @@ "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-r7/rule.spec.tsx", "test/sia-r9/rule.spec.tsx", From 4f5fddd3d04328f69f66e944080c5cad82191610 Mon Sep 17 00:00:00 2001 From: elenamongelli <52746406+elenamongelli@users.noreply.github.com> Date: Fri, 7 May 2021 13:04:55 +0200 Subject: [PATCH 12/46] R6 test added (#795) --- packages/alfa-rules/test/sia-r6/rule.spec.tsx | 80 +++++++++++++++++++ packages/alfa-rules/tsconfig.json | 3 +- 2 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 packages/alfa-rules/test/sia-r6/rule.spec.tsx 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..3d5b65c05e --- /dev/null +++ b/packages/alfa-rules/test/sia-r6/rule.spec.tsx @@ -0,0 +1,80 @@ +import { test } from "@siteimprove/alfa-test"; + +import { Document } from "@siteimprove/alfa-dom"; + +import R6, { Outcomes } from "../../src/sia-r6/rule"; + +import { evaluate } from "../common/evaluate"; +import { passed, failed, inapplicable } from "../common/outcome"; + +test("evaluate() passes an html element which has identical primary language subtags for its lang and xml:lang attributes.", async (t) => { + const target = ; + + const document = Document.of([target]); + + t.deepEqual(await evaluate(R6, { document }), [ + passed(R6, target, { + 1: Outcomes.HasMatchingLanguages, + }), + ]); + }); + + test("evaluate() passes an html element which has identical primary language subtags for its lang and xml:lang attributes. The extended language subtags also match.", async (t) => { + const target = ; + + const document = Document.of([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 = ; + + const document = Document.of([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 for its lang and xml:lang attributes. The extended language subtags do match", async (t) => { + const target = ; + + const document = Document.of([target]); + + t.deepEqual(await evaluate(R6, { document }), [ + failed(R6, target, { + 1: Outcomes.HasNonMatchingLanguages, + }), + ]); + }); + + test(`evaluate() is inapplicable to svg elements.`, async (t) => { + const target = ; + + const document = Document.of([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 = ; + + const document = Document.of([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 = ; + + const document = Document.of([target]); + +t.deepEqual(await evaluate(R6, { document }), [inapplicable(R6)]); +}); \ No newline at end of file diff --git a/packages/alfa-rules/tsconfig.json b/packages/alfa-rules/tsconfig.json index c3c141edb8..e94faed657 100644 --- a/packages/alfa-rules/tsconfig.json +++ b/packages/alfa-rules/tsconfig.json @@ -153,9 +153,10 @@ "test/common/predicate/is-visible.spec.tsx", "test/common/expectation/get-colors.spec.tsx", "test/sia-r1/rule.spec.tsx", - "test/sia-r2/rule.spec.tsx", + "test/sia-r2/rule.spec.tsx", "test/sia-r3/rule.spec.tsx", "test/sia-r4/rule.spec.tsx", + "test/sia-r6/rule.spec.tsx", "test/sia-r7/rule.spec.tsx", "test/sia-r9/rule.spec.tsx", "test/sia-r10/rule.spec.tsx", From b5f2dd69900156fb3742f43a9e740cce55bdcf62 Mon Sep 17 00:00:00 2001 From: Jean-Yves Moyen Date: Fri, 7 May 2021 11:45:07 +0200 Subject: [PATCH 13/46] Run prettier --- .../alfa-rules/test/sia-r17/rule.spec.tsx | 226 ++++++++++-------- 1 file changed, 120 insertions(+), 106 deletions(-) diff --git a/packages/alfa-rules/test/sia-r17/rule.spec.tsx b/packages/alfa-rules/test/sia-r17/rule.spec.tsx index 5b73a8e43c..6c03875cde 100644 --- a/packages/alfa-rules/test/sia-r17/rule.spec.tsx +++ b/packages/alfa-rules/test/sia-r17/rule.spec.tsx @@ -20,112 +20,126 @@ test(`evaluate() passes an element which is not focusable by default`, async (t) }); test(`evaluate() passes an element which content is hidden`, async (t) => { - const target = ; - - const document = Document.of([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 = ; - - const document = Document.of([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 = ; - - const document = Document.of([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 = ; - - const document = Document.of([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 =; - - const document = Document.of([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 =; - - const document = Document.of([target]); - - t.deepEqual(await evaluate(R17, { document }), [ - failed(R17, target, { - 1: Outcomes.IsTabbable, - }), - ]); - }); - - test(`evaluate() fails a focusable summary element`, async (t) => { - const target = ; - - const document = Document.of([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 =
-

Some text

-
; - - const document = Document.of([target]); - - t.deepEqual(await evaluate(R17, { document }), [inapplicable(R17)]); - }); + const target = ( + + ); + const document = Document.of([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 = ( + + ); + + const document = Document.of([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 = ; + + const document = Document.of([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 = ( + + ); + const document = Document.of([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 = ( + + ); + + const document = Document.of([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 = ( + + ); + + const document = Document.of([target]); + + t.deepEqual(await evaluate(R17, { document }), [ + failed(R17, target, { + 1: Outcomes.IsTabbable, + }), + ]); +}); + +test(`evaluate() fails a focusable summary element`, async (t) => { + const target = ( + + ); + + const document = Document.of([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 = ( +
+

Some text

+
+ ); + + const document = Document.of([target]); + + t.deepEqual(await evaluate(R17, { document }), [inapplicable(R17)]); +}); From 0c2977c588a5a664dbaab834ec64bd46dbf4ebf6 Mon Sep 17 00:00:00 2001 From: Jean-Yves Moyen Date: Fri, 7 May 2021 16:00:57 +0200 Subject: [PATCH 14/46] Add --immutable flag to install instruction, add intructions for running tests --- README.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 58e2ba2d57..7734771cf6 100644 --- a/README.md +++ b/README.md @@ -131,7 +131,7 @@ Alfa will run in any [ECMAScript 2018](https://www.ecma-international.org/ecma-2 In order to build Alfa, a recent version (>= 12) of [Node.js](https://nodejs.org/) is required in addition to the [Yarn](https://yarnpkg.com/) package manager. Once Node.js and Yarn are installed, go ahead and install the Alfa development dependencies: ```console -$ yarn install +$ yarn install --immutable ``` When done, you can start a watcher that watches source files for changes and kicks off the associated build steps when they change: @@ -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 From f66b35e188f0dd0479c1f43eaafad048f7c3fb05 Mon Sep 17 00:00:00 2001 From: Jean-Yves Moyen Date: Tue, 11 May 2021 10:28:04 +0200 Subject: [PATCH 15/46] Simplify double negative --- packages/alfa-rules/src/sia-r87/rule.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/alfa-rules/src/sia-r87/rule.ts b/packages/alfa-rules/src/sia-r87/rule.ts index 2f00ef0991..279499a941 100644 --- a/packages/alfa-rules/src/sia-r87/rule.ts +++ b/packages/alfa-rules/src/sia-r87/rule.ts @@ -100,7 +100,7 @@ export default Rule.Atomic.of({ () => Outcomes.FirstTabbableIsNotLink, () => expectation( - element.none(not(isIgnored(device))), + element.some(isIgnored(device)), () => Outcomes.FirstTabbableIsIgnored, () => expectation( From 6bc044859e075d278e333a405ad1ddd62bff165c Mon Sep 17 00:00:00 2001 From: elenamongelli <52746406+elenamongelli@users.noreply.github.com> Date: Tue, 11 May 2021 14:26:28 +0200 Subject: [PATCH 16/46] SIA R5: add test (#794) * Test r5 added * Apply suggestions from code review Co-authored-by: Jean-Yves Moyen * variable const target changed to html Co-authored-by: Jean-Yves Moyen --- packages/alfa-rules/test/sia-r5/rule.spec.tsx | 51 +++++++++++++++++++ packages/alfa-rules/tsconfig.json | 1 + 2 files changed, 52 insertions(+) create mode 100644 packages/alfa-rules/test/sia-r5/rule.spec.tsx 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..a24953638e --- /dev/null +++ b/packages/alfa-rules/test/sia-r5/rule.spec.tsx @@ -0,0 +1,51 @@ +import { test } from "@siteimprove/alfa-test"; + +import { Document } from "@siteimprove/alfa-dom"; + +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 = ; + + const document = Document.of([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 = ; + + const document = Document.of([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 = ; + + const document = Document.of([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 = ; + const document = Document.of([html]); + + t.deepEqual(await evaluate(R5, { document }), [inapplicable(R5)]); +}); diff --git a/packages/alfa-rules/tsconfig.json b/packages/alfa-rules/tsconfig.json index e94faed657..cddf17b7d7 100644 --- a/packages/alfa-rules/tsconfig.json +++ b/packages/alfa-rules/tsconfig.json @@ -156,6 +156,7 @@ "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-r9/rule.spec.tsx", From 96f6a4c06d4c351217df4e7b8a80b6da449665c2 Mon Sep 17 00:00:00 2001 From: elenamongelli <52746406+elenamongelli@users.noreply.github.com> Date: Mon, 17 May 2021 08:47:28 +0200 Subject: [PATCH 17/46] SIA R12: Add Test (#800) * Pass tests for R12 implemented, starting with failed * Update rule.spec.tsx * Test rule 12 finished * Apply suggestions from code review Co-authored-by: Jean-Yves Moyen * Text sintax improved Co-authored-by: Jean-Yves Moyen --- .../alfa-rules/test/sia-r12/rule.spec.tsx | 92 +++++++++++++++++++ packages/alfa-rules/tsconfig.json | 1 + 2 files changed, 93 insertions(+) create mode 100644 packages/alfa-rules/test/sia-r12/rule.spec.tsx 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..a3de061309 --- /dev/null +++ b/packages/alfa-rules/test/sia-r12/rule.spec.tsx @@ -0,0 +1,92 @@ +import { test } from "@siteimprove/alfa-test"; + +import { Document } from "@siteimprove/alfa-dom"; + +import R12, { Outcomes } from "../../src/sia-r12/rule"; + +import { evaluate } from "../common/evaluate"; +import { passed, failed, inapplicable } from "../common/outcome"; + +test(`evaluates() passes a ; + + const document = Document.of([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 = ; + + const document = Document.of([input]); + + t.deepEqual(await evaluate(R12, { document }), [ + passed(R12, input, { + 1: Outcomes.HasName, + }), + ]); +}); + +test(`evaluates() passes a ; + + const document = Document.of([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 = ; + + const document = Document.of([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 = ; + + const document = Document.of([target]); + + t.deepEqual(await evaluate(R12, { document }), [ + failed(R12, target, { + 1: Outcomes.HasNoName, + }), + ]); +}); + +test(`evaluate() is inapplicable to image buttons`, async (t) => { + const target = ; + + const document = Document.of([target]); + + t.deepEqual(await evaluate(R12, { document }), [inapplicable(R12)]); +}); + +test(`evaluate() is inapplicabile to element with no button role`, async (t) => { + const target =
Press Here
; + + const document = Document.of([target]); + + t.deepEqual(await evaluate(R12, { document }), [inapplicable(R12)]); +}); + +test(`evaluate() is inapplicabile to button element with none role`, async (t) => { + const target = ; + + const document = Document.of([target]); + + t.deepEqual(await evaluate(R12, { document }), [inapplicable(R12)]); +}); diff --git a/packages/alfa-rules/tsconfig.json b/packages/alfa-rules/tsconfig.json index cddf17b7d7..33b77b3d51 100644 --- a/packages/alfa-rules/tsconfig.json +++ b/packages/alfa-rules/tsconfig.json @@ -162,6 +162,7 @@ "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", From 46eed6fa2d3ec404b21799170f26080e376d5d9e Mon Sep 17 00:00:00 2001 From: Jean-Yves Moyen Date: Mon, 17 May 2021 14:40:08 +0200 Subject: [PATCH 18/46] Fix hasRole to pick the role from the accessible node (#805) --- .../alfa-rules/src/common/predicate/has-role.ts | 12 +++++++++--- packages/alfa-rules/src/sia-r10/rule.ts | 2 +- packages/alfa-rules/src/sia-r11/rule.ts | 2 +- packages/alfa-rules/src/sia-r12/rule.ts | 2 +- packages/alfa-rules/src/sia-r14/rule.ts | 5 ++++- packages/alfa-rules/src/sia-r16/rule.ts | 4 +++- packages/alfa-rules/src/sia-r18/rule.ts | 2 +- packages/alfa-rules/src/sia-r2/rule.ts | 2 +- packages/alfa-rules/src/sia-r41/rule.ts | 2 +- packages/alfa-rules/src/sia-r42/rule.ts | 2 +- packages/alfa-rules/src/sia-r45/rule.ts | 5 ++++- packages/alfa-rules/src/sia-r46/rule.ts | 4 ++-- packages/alfa-rules/src/sia-r53/rule.ts | 2 +- packages/alfa-rules/src/sia-r57/rule.ts | 2 +- packages/alfa-rules/src/sia-r59/rule.ts | 4 ++-- packages/alfa-rules/src/sia-r61/rule.ts | 2 +- packages/alfa-rules/src/sia-r62/rule.ts | 11 +++++++---- packages/alfa-rules/src/sia-r64/rule.ts | 2 +- packages/alfa-rules/src/sia-r66/rule.ts | 4 ++-- packages/alfa-rules/src/sia-r68/rule.ts | 2 +- packages/alfa-rules/src/sia-r69/rule.ts | 4 ++-- packages/alfa-rules/src/sia-r71/rule.ts | 2 +- packages/alfa-rules/src/sia-r72/rule.ts | 2 +- packages/alfa-rules/src/sia-r73/rule.ts | 2 +- packages/alfa-rules/src/sia-r74/rule.ts | 2 +- packages/alfa-rules/src/sia-r8/rule.ts | 1 + packages/alfa-rules/src/sia-r80/rule.ts | 2 +- packages/alfa-rules/src/sia-r81/rule.ts | 13 ++++++++----- packages/alfa-rules/src/sia-r82/rule.ts | 1 + packages/alfa-rules/src/sia-r85/rule.ts | 2 +- packages/alfa-rules/src/sia-r87/rule.ts | 12 ++++++------ packages/alfa-rules/src/sia-r90/rule.ts | 2 +- packages/alfa-rules/src/sia-r94/rule.ts | 2 +- .../test/common/predicate/has-role.spec.tsx | 16 ++++++++++++++++ packages/alfa-rules/tsconfig.json | 3 ++- 35 files changed, 89 insertions(+), 50 deletions(-) create mode 100644 packages/alfa-rules/test/common/predicate/has-role.spec.tsx 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/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 7b173e833f..2bbde1a330 100644 --- a/packages/alfa-rules/src/sia-r14/rule.ts +++ b/packages/alfa-rules/src/sia-r14/rule.ts @@ -37,7 +37,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, }) diff --git a/packages/alfa-rules/src/sia-r16/rule.ts b/packages/alfa-rules/src/sia-r16/rule.ts index 04b2496261..115e84119f 100644 --- a/packages/alfa-rules/src/sia-r16/rule.ts +++ b/packages/alfa-rules/src/sia-r16/rule.ts @@ -27,7 +27,9 @@ 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))); }, 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..82c3ba8144 100644 --- a/packages/alfa-rules/src/sia-r41/rule.ts +++ b/packages/alfa-rules/src/sia-r41/rule.ts @@ -37,7 +37,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) ) 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-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 1c00bea762..e3ad7976c6 100644 --- a/packages/alfa-rules/src/sia-r62/rule.ts +++ b/packages/alfa-rules/src/sia-r62/rule.ts @@ -38,7 +38,7 @@ export default Rule.Atomic.of({ // 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 ) ) { @@ -55,7 +55,10 @@ 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; } @@ -78,7 +81,7 @@ export default Rule.Atomic.of({ flattened: true, }) .filter(isElement) - .find(hasRole("paragraph")) + .find(hasRole(device, "paragraph")) .get(); return { @@ -125,7 +128,7 @@ function hasNonLinkText(device: Device): Predicate { return children .filter(isElement) - .reject(hasRole((role) => role.is("link"))) + .reject(hasRole(device, (role) => role.is("link"))) .some(hasNonLinkText); }; } 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-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..4147ab152c 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"; @@ -40,12 +41,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) => { @@ -131,12 +134,12 @@ function normalize(input: string): string { * * {@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 +147,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 279499a941..fe5e7154ca 100644 --- a/packages/alfa-rules/src/sia-r87/rule.ts +++ b/packages/alfa-rules/src/sia-r87/rule.ts @@ -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.some(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/tsconfig.json b/packages/alfa-rules/tsconfig.json index 33b77b3d51..f7b0968623 100644 --- a/packages/alfa-rules/tsconfig.json +++ b/packages/alfa-rules/tsconfig.json @@ -151,9 +151,10 @@ "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-r2/rule.spec.tsx", "test/sia-r3/rule.spec.tsx", "test/sia-r4/rule.spec.tsx", "test/sia-r5/rule.spec.tsx", From 71d54829d3bf8c0259ff032cf8a26ba577a0fe09 Mon Sep 17 00:00:00 2001 From: Jean-Yves Moyen Date: Mon, 17 May 2021 15:10:55 +0200 Subject: [PATCH 19/46] Clean up --- packages/alfa-rules/src/sia-r87/rule.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/alfa-rules/src/sia-r87/rule.ts b/packages/alfa-rules/src/sia-r87/rule.ts index fe5e7154ca..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({ From 3129b13c1c0ddd142ea6b05bb0db61d17178a214 Mon Sep 17 00:00:00 2001 From: elenamongelli <52746406+elenamongelli@users.noreply.github.com> Date: Mon, 17 May 2021 15:27:47 +0200 Subject: [PATCH 20/46] R18 add test (#803) * Bump actions/create-release from 1 to 1.1.4 (#802) Bumps [actions/create-release](https://github.com/actions/create-release) from 1 to 1.1.4. - [Release notes](https://github.com/actions/create-release/releases) - [Commits](https://github.com/actions/create-release/compare/v1...v1.1.4) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * SIA R18: Added test Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Jean-Yves Moyen --- .../alfa-rules/test/sia-r18/rule.spec.tsx | 173 ++++++++++++++++++ packages/alfa-rules/tsconfig.json | 1 + 2 files changed, 174 insertions(+) create mode 100644 packages/alfa-rules/test/sia-r18/rule.spec.tsx 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..a48454e4dc --- /dev/null +++ b/packages/alfa-rules/test/sia-r18/rule.spec.tsx @@ -0,0 +1,173 @@ +import { test } from "@siteimprove/alfa-test"; + +import { Document } from "@siteimprove/alfa-dom"; + +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 = ; + + const document = Document.of([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, whose has aria-pressed state`, async (t) => { + const target = ( +
+ My button +
+ ); + + const document = Document.of([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 =
My busy div
; + + const document = Document.of([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, whose has aria-label state`, async (t) => { + const target = ( +
+ âś“ +
+ ); + + const document = Document.of([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, whose has aria-checked state`, async (t) => { + const target = ( +
+ My checkbox +
+ ); + + const document = Document.of([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, whose has aria-controls state`, async (t) => { + const target = ( + + ); + + const document = Document.of([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, whose has aria-controls and aria-expanded state`, async (t) => { + const target = ( + + ); + + const document = Document.of([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, whose has aria-expanded and aria-controls (empty) state`, async (t) => { + const target = ( + + ); + + const document = Document.of([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 = ( + + ); + + const document = Document.of([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, but it doesn't have any property`, async (t) => { + const target = ; + + const document = Document.of([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 state / property`, async (t) => { + const target =
A region of content
; + + const document = Document.of([target]); + + t.deepEqual(await evaluate(R18, { document }), [inapplicable(R18)]); +}); diff --git a/packages/alfa-rules/tsconfig.json b/packages/alfa-rules/tsconfig.json index f7b0968623..2f406e0f2c 100644 --- a/packages/alfa-rules/tsconfig.json +++ b/packages/alfa-rules/tsconfig.json @@ -169,6 +169,7 @@ "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-r21/rule.spec.tsx", "test/sia-r24/rule.spec.tsx", "test/sia-r38/rule.spec.tsx", From 1a8b05b7ec7b95a27bd356d39be9d513635f3eba Mon Sep 17 00:00:00 2001 From: elenamongelli <52746406+elenamongelli@users.noreply.github.com> Date: Tue, 18 May 2021 16:44:43 +0200 Subject: [PATCH 21/46] SIA-R19: Add test (#807) * Test for R19 Added * Added test for rule 19 * Apply suggestions from code review Co-authored-by: Jean-Yves Moyen Co-authored-by: Jean-Yves Moyen --- .../alfa-rules/test/sia-r19/rule.spec.tsx | 265 ++++++++++++++++++ packages/alfa-rules/tsconfig.json | 1 + 2 files changed, 266 insertions(+) create mode 100644 packages/alfa-rules/test/sia-r19/rule.spec.tsx 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..cb1adf3955 --- /dev/null +++ b/packages/alfa-rules/test/sia-r19/rule.spec.tsx @@ -0,0 +1,265 @@ +import { h } from "@siteimprove/alfa-dom/h"; +import { test } from "@siteimprove/alfa-test"; + +import { Document } from "@siteimprove/alfa-dom"; +import { Response } from "@siteimprove/alfa-http"; +import { URL } from "@siteimprove/alfa-url"; + +import R19, { Outcomes } from "../../src/sia-r19/rule"; + +import { Group } from "../../src/common/group"; + +import { evaluate } from "../common/evaluate"; +import { oracle } from "../common/oracle"; +import { passed, failed, inapplicable } from "../common/outcome"; + +test("evaluate() passes a div element with aria-required property, whose has a valid true value", async (t) => { + const target = ( +
+ ); + + const document = Document.of([target]); + + t.deepEqual(await evaluate(R19, { document }), [ + passed(R19, target.attribute("aria-required").get(), { + 1: Outcomes.HasValidValue, + }), + ]); +}); + +test("evaluate() passes a div element with aria-expanded state, whose has a valid undefined value", async (t) => { + const target = ( +
+ A button +
+ ); + + const document = Document.of([target]); + + t.deepEqual(await evaluate(R19, { document }), [ + passed(R19, target.attribute("aria-expanded").get(), { + 1: Outcomes.HasValidValue, + }), + ]); +}); + +test("evaluate() passes a div element with aria-pressed state, whose has a valid tristate value", async (t) => { + const target = ( +
+ An other button +
+ ); + + const document = Document.of([target]); + + t.deepEqual(await evaluate(R19, { document }), [ + passed(R19, target.attribute("aria-pressed").get(), { + 1: Outcomes.HasValidValue, + }), + ]); +}); + +test("evaluate() passes a div element with aria-errormessage property, whose has a valid ID reference value", async (t) => { + const target = ( +
+ ); + + const document = Document.of([target]); + + t.deepEqual(await evaluate(R19, { document }), [ + passed(R19, target.attribute("aria-errormessage").get(), { + 1: Outcomes.HasValidValue, + }), + ]); +}); + +test("evaluate() passes a div element with aria-rowindex property, whose has a valid integer value", async (t) => { + const target = ( +
+ Fred +
+ ); + + const document = Document.of([target]); + + t.deepEqual(await evaluate(R19, { document }), [ + passed(R19, target.attribute("aria-rowindex").get(), { + 1: Outcomes.HasValidValue, + }), + ]); +}); + +test("evaluate() passes a div element with aria-valuemin, aria-valuemax and aria-valuenow properties with valid number values", async (t) => { + const target = ( +
+ ); + + const document = Document.of([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 a div element with aria-placeholder property, whose has a valid string value", async (t) => { + const target = ( +
+ MM-DD-YYYY +
+ ); + + const document = Document.of([target]); + + t.deepEqual(await evaluate(R19, { document }), [ + passed(R19, target.attribute("aria-placeholder").get(), { + 1: Outcomes.HasValidValue, + }), + ]); +}); + +test("evaluate() passes a div element with aria-dropeffect property, whose has a valid token list value", async (t) => { + const target =
; + + const document = Document.of([target]); + + t.deepEqual(await evaluate(R19, { document }), [ + passed(R19, target.attribute("aria-dropeffect").get(), { + 1: Outcomes.HasValidValue, + }), + ]); +}); + +test("evaluate() fails a div element with aria-expanded state, whose has a invalid true/false/undefined value", async (t) => { + const target = ( +
+ A button +
+ ); + + const document = Document.of([target]); + + t.deepEqual(await evaluate(R19, { document }), [ + failed(R19, target.attribute("aria-expanded").get(), { + 1: Outcomes.HasNoValidValue, + }), + ]); +}); + +test("evaluate() fails a div element with aria-pressed state, whose has a invalid tristate value", async (t) => { + const target = ( +
+ An other button +
+ ); + + const document = Document.of([target]); + + t.deepEqual(await evaluate(R19, { document }), [ + failed(R19, target.attribute("aria-pressed").get(), { + 1: Outcomes.HasNoValidValue, + }), + ]); +}); + +test("evaluate() fails a div element with aria-rowindex property, whose has a invalid integer value", async (t) => { + const target = ( +
+ Fred +
+ ); + + const document = Document.of([target]); + + t.deepEqual(await evaluate(R19, { document }), [ + failed(R19, target.attribute("aria-rowindex").get(), { + 1: Outcomes.HasNoValidValue, + }), + ]); +}); + +test("evaluate() fails a div element with aria-live property, whose has a invalid token value", async (t) => { + const target =
; + + const document = Document.of([target]); + + t.deepEqual(await evaluate(R19, { document }), [ + failed(R19, target.attribute("aria-live").get(), { + 1: Outcomes.HasNoValidValue, + }), + ]); +}); + +test("evaluate() fails a div element with aria-rowindex property, whose has a invalid integer value", async (t) => { + const target = ( +
+ ); + + const document = Document.of([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 a div element with aria-errormessage property, whose has a invalid ID reference value", async (t) => { + const target =
; + + const document = Document.of([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 states or properties", async (t) => { + const document = Document.of([
Some Content
]); + + t.deepEqual(await evaluate(R19, { document }), [inapplicable(R19)]); +}); + +test("evaluate() is inapplicable when aria-checked state has empty value", async (t) => { + const document = Document.of([ +
+ Accept terms and conditions +
, + ]); + + t.deepEqual(await evaluate(R19, { document }), [inapplicable(R19)]); +}); diff --git a/packages/alfa-rules/tsconfig.json b/packages/alfa-rules/tsconfig.json index 2f406e0f2c..b8ff9f41ad 100644 --- a/packages/alfa-rules/tsconfig.json +++ b/packages/alfa-rules/tsconfig.json @@ -170,6 +170,7 @@ "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-r21/rule.spec.tsx", "test/sia-r24/rule.spec.tsx", "test/sia-r38/rule.spec.tsx", From 0378d6f9a33cbb3f791d197ffd799312605cbd68 Mon Sep 17 00:00:00 2001 From: elenamongelli <52746406+elenamongelli@users.noreply.github.com> Date: Tue, 18 May 2021 16:46:59 +0200 Subject: [PATCH 22/46] SIA-R8: Added tests (#806) * Added test to Rule 8 * Apply suggestions from code review Co-authored-by: Jean-Yves Moyen Co-authored-by: Jean-Yves Moyen --- packages/alfa-rules/test/sia-r8/rule.spec.tsx | 194 ++++++++++++++++++ packages/alfa-rules/tsconfig.json | 1 + 2 files changed, 195 insertions(+) create mode 100644 packages/alfa-rules/test/sia-r8/rule.spec.tsx 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..30c80ee53c --- /dev/null +++ b/packages/alfa-rules/test/sia-r8/rule.spec.tsx @@ -0,0 +1,194 @@ +import { test } from "@siteimprove/alfa-test"; + +import { Document } from "@siteimprove/alfa-dom"; + +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 = ; + + const label = ( + + ); + + const document = Document.of([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 = ; + + const document = Document.of([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 = ( + + ); + + const label = ; + + const document = Document.of([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 = ; + + const label =
Country
; + + const document = Document.of([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 = ; + + const label = ; + + const document = Document.of([label, target]); + + t.deepEqual(await evaluate(R8, { document }), [ + passed(R8, target, { + 1: Outcomes.HasName, + }), + ]); +}); + +test("evaluate() passes a input element with combobox role, whose has an aria-label attribute", async (t) => { + const target = ( +
+ England +
+ ); + + const document = Document.of([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 = ; + + const document = Document.of([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 = ; + + const document = Document.of([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 element with no text", async (t) => { + const target = ( + + ); + + const label =
; + + const document = Document.of([label, target]); + + t.deepEqual(await evaluate(R8, { document }), [ + failed(R8, target, { + 1: Outcomes.HasNoName, + }), + ]); +}); + +test("evaluate() fails a text box element with no accessible name", async (t) => { + const target =
; + + const label = ( + + ); + const document = Document.of([label, target]); + + t.deepEqual(await evaluate(R8, { document }), [ + failed(R8, target, { + 1: Outcomes.HasNoName, + }), + ]); +}); + +test("evaluate() is inapplicable for an input element with aria-hidden", async (t) => { + const target = ; + + const document = Document.of([target]); + + t.deepEqual(await evaluate(R8, { document }), [inapplicable(R8)]); +}); + +test("evaluate() is inapplicable for a disabled element", async (t) => { + const target = ( + + ); + + const document = Document.of([target]); + + t.deepEqual(await evaluate(R8, { document }), [inapplicable(R8)]); +}); + +test("evaluate() is inapplicable for an input element which is not displayed", async (t) => { + const target = ; + + const document = Document.of([target]); + + t.deepEqual(await evaluate(R8, { document }), [inapplicable(R8)]); +}); diff --git a/packages/alfa-rules/tsconfig.json b/packages/alfa-rules/tsconfig.json index b8ff9f41ad..109c2b1b9b 100644 --- a/packages/alfa-rules/tsconfig.json +++ b/packages/alfa-rules/tsconfig.json @@ -160,6 +160,7 @@ "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", From 98237ceb8af4f33a2693381ecaf0c3aff4d15b17 Mon Sep 17 00:00:00 2001 From: elenamongelli <52746406+elenamongelli@users.noreply.github.com> Date: Wed, 19 May 2021 11:16:03 +0200 Subject: [PATCH 23/46] R16 extended diagnostic (#804) * Set role name in extended diagnostic * Required attributes added in rule and test * Extended diagnostic added * Apply suggestions from code review Co-authored-by: Jean-Yves Moyen * Correctly send required attributes to diagnostic * Structure diagnostic JSON a bit Co-authored-by: Jean-Yves Moyen --- packages/alfa-aria/src/role.ts | 1 - packages/alfa-rules/src/sia-r16/rule.ts | 199 +++++++++++++++--- .../alfa-rules/test/sia-r16/rule.spec.tsx | 63 +++++- 3 files changed, 220 insertions(+), 43 deletions(-) diff --git a/packages/alfa-aria/src/role.ts b/packages/alfa-aria/src/role.ts index 18cb093671..2c837f3138 100644 --- a/packages/alfa-aria/src/role.ts +++ b/packages/alfa-aria/src/role.ts @@ -241,7 +241,6 @@ export class Role return false; } - /** * Get the implicit value of the specified attribute, if any. */ diff --git a/packages/alfa-rules/src/sia-r16/rule.ts b/packages/alfa-rules/src/sia-r16/rule.ts index 115e84119f..4d35abf0c7 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 { Attribute, 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"; @@ -34,11 +37,13 @@ export default Rule.Atomic.of({ }, 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()) ), }; }, @@ -46,41 +51,167 @@ 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 = []; + let found: 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, found) + ); + } - 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; + } else { + found.push(attribute); } } } + } + + return result + ? Ok.of( + RoleAndRequiredAttributes.of("", roleName, required, missing, found) + ) + : Err.of( + RoleAndRequiredAttributes.of("", roleName, required, missing, found) + ); +} + +export class RoleAndRequiredAttributes extends Diagnostic { + public static of( + message: string, + role: string = "", + requiredAttributes: ReadonlyArray = [], + missingAttributes: ReadonlyArray = [], + foundAttributes: ReadonlyArray = [] + ): RoleAndRequiredAttributes { + return new RoleAndRequiredAttributes( + message, + role, + requiredAttributes, + missingAttributes, + foundAttributes + ); + } + + private readonly _role: string; + private readonly _requiredAttributes: ReadonlyArray; + private readonly _missingAttributes: ReadonlyArray; + private readonly _foundAttributes: ReadonlyArray; + + private constructor( + message: string, + role: string, + requiredAttributes: ReadonlyArray, + missingAttributes: ReadonlyArray, + foundAttributes: ReadonlyArray + ) { + super(message); + this._role = role; + this._requiredAttributes = requiredAttributes; + this._missingAttributes = missingAttributes; + this._foundAttributes = foundAttributes; + } + + public get role(): string { + return this._role; + } - return true; - }; + public get requiredAttributes(): ReadonlyArray { + return this._requiredAttributes; + } + + public get missingAttributes(): ReadonlyArray { + return this._missingAttributes; + } + + public get foundAttributes(): ReadonlyArray { + return this._foundAttributes; + } + + public withMessage(message: string): RoleAndRequiredAttributes { + return new RoleAndRequiredAttributes( + message, + this._role, + this._requiredAttributes, + this._missingAttributes, + this._foundAttributes + ); + } + + 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) && + Array.equals(value._foundAttributes, this._foundAttributes) + ); + } + + public toJSON(): RoleAndRequiredAttributes.JSON { + return { + ...super.toJSON(), + role: this._role, + attributes: { + required: Array.copy(this._requiredAttributes), + missing: Array.copy(this._missingAttributes), + found: Array.copy(this._foundAttributes), + }, + }; + } +} + +namespace RoleAndRequiredAttributes { + export interface JSON extends Diagnostic.JSON { + role: string; + attributes: { + required: Array; + missing: Array; + found: 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/test/sia-r16/rule.spec.tsx b/packages/alfa-rules/test/sia-r16/rule.spec.tsx index a7af36b0a4..8f1ca27fa9 100644 --- a/packages/alfa-rules/test/sia-r16/rule.spec.tsx +++ b/packages/alfa-rules/test/sia-r16/rule.spec.tsx @@ -2,7 +2,10 @@ 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"; @@ -15,7 +18,15 @@ test(`evaluate() passes a
element with a role of checkbox and an t.deepEqual(await evaluate(R16, { document }), [ passed(R16, target, { - 1: Outcomes.HasAllStates, + 1: Outcomes.HasAllStates( + RoleAndRequiredAttributes.of( + "", + "checkbox", + ["aria-checked"], + [], + ["aria-checked"] + ) + ), }), ]); }); @@ -27,7 +38,15 @@ test(`evaluate() passes an element with a type of checkbox`, async (t) = t.deepEqual(await evaluate(R16, { document }), [ passed(R16, target, { - 1: Outcomes.HasAllStates, + 1: Outcomes.HasAllStates( + RoleAndRequiredAttributes.of( + "", + "checkbox", + ["aria-checked"], + [], + ["aria-checked"] + ) + ), }), ]); }); @@ -39,7 +58,9 @@ test(`evaluate() passes an
element`, async (t) => { t.deepEqual(await evaluate(R16, { document }), [ passed(R16, target, { - 1: Outcomes.HasAllStates, + 1: Outcomes.HasAllStates( + RoleAndRequiredAttributes.of("", "separator", [], [], []) + ), }), ]); }); @@ -51,7 +72,9 @@ test(`evaluate() passes a non-focusable
element with a role of separator`, t.deepEqual(await evaluate(R16, { document }), [ passed(R16, target, { - 1: Outcomes.HasAllStates, + 1: Outcomes.HasAllStates( + RoleAndRequiredAttributes.of("", "separator", [], [], []) + ), }), ]); }); @@ -64,7 +87,15 @@ test(`evaluate() passes a focusable
element with a role of separator and t.deepEqual(await evaluate(R16, { document }), [ passed(R16, target, { - 1: Outcomes.HasAllStates, + 1: Outcomes.HasAllStates( + RoleAndRequiredAttributes.of( + "", + "separator", + ["aria-valuenow"], + [], + ["aria-valuenow"] + ) + ), }), ]); }); @@ -77,7 +108,15 @@ test(`evaluate() fails a
element with a role of checkbox and no t.deepEqual(await evaluate(R16, { document }), [ failed(R16, target, { - 1: Outcomes.HasNotAllStates, + 1: Outcomes.HasNotAllStates( + RoleAndRequiredAttributes.of( + "", + "checkbox", + ["aria-checked"], + ["aria-checked"], + [] + ) + ), }), ]); }); @@ -90,7 +129,15 @@ test(`evaluate() fails a focusable
element with a role of separator and no t.deepEqual(await evaluate(R16, { document }), [ failed(R16, target, { - 1: Outcomes.HasNotAllStates, + 1: Outcomes.HasNotAllStates( + RoleAndRequiredAttributes.of( + "", + "separator", + ["aria-valuenow"], + ["aria-valuenow"], + [] + ) + ), }), ]); }); From 6ec7b83ba148a092bdeba2a35942ceffd158f7ee Mon Sep 17 00:00:00 2001 From: Jean-Yves Moyen Date: Wed, 19 May 2021 12:37:33 +0200 Subject: [PATCH 24/46] Clean up test descriptions a bit --- .../alfa-rules/test/sia-r11/rule.spec.tsx | 2 +- .../alfa-rules/test/sia-r12/rule.spec.tsx | 12 +- .../alfa-rules/test/sia-r17/rule.spec.tsx | 3 +- .../alfa-rules/test/sia-r18/rule.spec.tsx | 20 +-- .../alfa-rules/test/sia-r19/rule.spec.tsx | 58 +++---- packages/alfa-rules/test/sia-r3/rule.spec.tsx | 152 +++++++++--------- packages/alfa-rules/test/sia-r4/rule.spec.tsx | 91 ++++++----- packages/alfa-rules/test/sia-r5/rule.spec.tsx | 3 +- packages/alfa-rules/test/sia-r6/rule.spec.tsx | 135 +++++++++------- packages/alfa-rules/test/sia-r8/rule.spec.tsx | 16 +- 10 files changed, 249 insertions(+), 243 deletions(-) diff --git a/packages/alfa-rules/test/sia-r11/rule.spec.tsx b/packages/alfa-rules/test/sia-r11/rule.spec.tsx index ce148fd6b4..70c3ced307 100644 --- a/packages/alfa-rules/test/sia-r11/rule.spec.tsx +++ b/packages/alfa-rules/test/sia-r11/rule.spec.tsx @@ -34,7 +34,7 @@ test(`evaluate() is inapplicable when there is no link`, async (t) => { test(`evaluate() passes an image link with name given by the alt text`, async (t) => { const target = ( - + Foo ); diff --git a/packages/alfa-rules/test/sia-r12/rule.spec.tsx b/packages/alfa-rules/test/sia-r12/rule.spec.tsx index a3de061309..e31fc15b91 100644 --- a/packages/alfa-rules/test/sia-r12/rule.spec.tsx +++ b/packages/alfa-rules/test/sia-r12/rule.spec.tsx @@ -7,7 +7,7 @@ import R12, { Outcomes } from "../../src/sia-r12/rule"; import { evaluate } from "../common/evaluate"; import { passed, failed, inapplicable } from "../common/outcome"; -test(`evaluates() passes a ; const document = Document.of([target]); @@ -19,7 +19,8 @@ test(`evaluates() passes a ; const document = Document.of([target]); @@ -75,7 +77,7 @@ test(`evaluate() is inapplicable to image buttons`, async (t) => { t.deepEqual(await evaluate(R12, { document }), [inapplicable(R12)]); }); -test(`evaluate() is inapplicabile to element with no button role`, async (t) => { +test(`evaluate() is inapplicable to element with no button role`, async (t) => { const target =
Press Here
; const document = Document.of([target]); @@ -83,7 +85,7 @@ test(`evaluate() is inapplicabile to element with no button role`, async (t) => t.deepEqual(await evaluate(R12, { document }), [inapplicable(R12)]); }); -test(`evaluate() is inapplicabile to button element with none role`, async (t) => { +test(`evaluate() is inapplicable to button element with none role`, async (t) => { const target = ; const document = Document.of([target]); diff --git a/packages/alfa-rules/test/sia-r17/rule.spec.tsx b/packages/alfa-rules/test/sia-r17/rule.spec.tsx index 6c03875cde..9e3ea750d0 100644 --- a/packages/alfa-rules/test/sia-r17/rule.spec.tsx +++ b/packages/alfa-rules/test/sia-r17/rule.spec.tsx @@ -37,7 +37,8 @@ test(`evaluate() passes an element which content is hidden`, async (t) => { ]); }); -test(`evaluate() passes an element whose content is taken out of sequential focus order using tabindex`, async (t) => { +test(`evaluate() passes an element whose content is taken out of sequential + focus order using tabindex`, async (t) => { const target = (