Skip to content

Commit

Permalink
Implement SIA R56 (#829)
Browse files Browse the repository at this point in the history
* Refactor normalize
* Mark members of Group as readonly
* Write R56
* Add extended diagnostic
  • Loading branch information
Jym77 authored Jun 9, 2021
1 parent 452bc78 commit b743bc9
Show file tree
Hide file tree
Showing 9 changed files with 257 additions and 35 deletions.
4 changes: 2 additions & 2 deletions packages/alfa-rules/src/common/group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ export class Group<T>
return new Group(Array.from(members));
}

private readonly _members: Array<T>;
private readonly _members: ReadonlyArray<T>;

private constructor(members: Array<T>) {
private constructor(members: ReadonlyArray<T>) {
this._members = members;
}

Expand Down
3 changes: 3 additions & 0 deletions packages/alfa-rules/src/common/normalize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function normalize(input: string): string {
return input.trim().toLowerCase().replace(/\s+/g, " ");
}
20 changes: 10 additions & 10 deletions packages/alfa-rules/src/sia-r14/rule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,16 @@ import { Page } from "@siteimprove/alfa-web";

import { expectation } from "../common/expectation";

import { hasAccessibleName } from "../common/predicate/has-accessible-name";
import { hasAttribute } from "../common/predicate/has-attribute";
import { hasDescendant } from "../common/predicate/has-descendant";
import { hasRole } from "../common/predicate/has-role";
import { isFocusable } from "../common/predicate/is-focusable";
import { isPerceivable } from "../common/predicate/is-perceivable";
import { normalize } from "../common/normalize";

import {
hasAccessibleName,
hasAttribute,
hasDescendant,
hasRole,
isFocusable,
isPerceivable,
} from "../common/predicate";

const { isElement, hasNamespace } = Element;
const { isText } = Text;
Expand Down Expand Up @@ -72,10 +76,6 @@ export default Rule.Atomic.of<Page, Element>({
},
});

function normalize(input: string): string {
return input.trim().toLowerCase().replace(/\s+/g, " ");
}

function getPerceivableTextContent(element: Element, device: Device): string {
return normalize(
element
Expand Down
7 changes: 2 additions & 5 deletions packages/alfa-rules/src/sia-r15/rule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ import { hasNonEmptyAccessibleName } from "../common/predicate/has-non-empty-acc
import { isIgnored } from "../common/predicate/is-ignored";
import { referenceSameResource } from "../common/predicate/reference-same-resource";

import { Question } from "../common/question";
import { Group } from "../common/group";
import { normalize } from "../common/normalize";
import { Question } from "../common/question";

const { isElement, hasName, hasNamespace } = Element;
const { and, not } = Predicate;
Expand Down Expand Up @@ -107,7 +108,3 @@ export namespace Outcomes {
)
);
}

function normalize(input: string): string {
return input.trim().toLowerCase().replace(/\s+/g, " ");
}
17 changes: 8 additions & 9 deletions packages/alfa-rules/src/sia-r41/rule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,16 @@ import { Page } from "@siteimprove/alfa-web";

import { expectation } from "../common/expectation";

import { hasNonEmptyAccessibleName } from "../common/predicate/has-non-empty-accessible-name";
import { hasRole } from "../common/predicate/has-role";
import { isIgnored } from "../common/predicate/is-ignored";
import {
hasNonEmptyAccessibleName,
hasRole,
isIgnored,
referenceSameResource,
} from "../common/predicate";

import { Question } from "../common/question";
import { Group } from "../common/group";
import { referenceSameResource } from "../common/predicate/reference-same-resource";
import { normalize } from "../common/normalize";
import { Question } from "../common/question";

const { isElement, hasNamespace } = Element;
const { flatten } = Iterable;
Expand Down Expand Up @@ -118,7 +121,3 @@ export namespace Outcomes {
)
);
}

function normalize(input: string): string {
return input.trim().toLowerCase().replace(/\s+/g, " ");
}
166 changes: 166 additions & 0 deletions packages/alfa-rules/src/sia-r56/rule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import { Diagnostic, Rule } from "@siteimprove/alfa-act";
import { Node, Role } from "@siteimprove/alfa-aria";
import { Array } from "@siteimprove/alfa-array";
import { Element, Namespace } from "@siteimprove/alfa-dom";
import { Iterable } from "@siteimprove/alfa-iterable";
import { List } from "@siteimprove/alfa-list";
import { Map } from "@siteimprove/alfa-map";
import { Option } from "@siteimprove/alfa-option";
import { Predicate } from "@siteimprove/alfa-predicate";
import { Err, Ok } from "@siteimprove/alfa-result";
import { Page } from "@siteimprove/alfa-web";

import { expectation } from "../common/expectation";
import { Group } from "../common/group";
import { normalize } from "../common/normalize";

import { hasRole, isIgnored } from "../common/predicate";

const { and, equals, not } = Predicate;
const { hasNamespace } = Element;

export default Rule.Atomic.of<Page, Group<Element>>({
uri: "https://siteimprove.github.io/sanshikan/rules/sia-r56.html",
evaluate({ device, document }) {
return {
applicability() {
return document
.descendants({ flattened: true, nested: true })
.filter(Element.isElement)
.filter(
and(
hasNamespace(equals(Namespace.HTML)),
not(isIgnored(device)),
hasRole(device, (role) => role.is("landmark"))
)
)
.reduce((groups, landmark) => {
// since we already have filtered by having a landmark role, we can
// safely get the role.
const role = Node.from(landmark, device).role.get()!;

groups = groups.set(
role,
groups
.get(role)
.getOrElse(() => List.empty<Element>())
.append(landmark)
);

return groups;
}, Map.empty<Role, List<Element>>())
.filter((elements) => elements.size > 1)
.map(Group.of)
.values();
},

expectations(target) {
// empty groups have been filtered out already, so we can safely get the
// first element
const role = Node.from(
Iterable.first(target).get()!,
device
).role.get()!.name;

const byNames = [...target]
.reduce((groups, landmark) => {
const name = Node.from(landmark, device).name.map((name) =>
normalize(name.value)
);
groups = groups.set(
name,
groups
.get(name)
.getOrElse(() => List.empty<Element>())
.append(landmark)
);

return groups;
}, Map.empty<Option<string>, List<Element>>())
.filter((landmarks) => landmarks.size > 1);

return {
1: expectation(
byNames.size === 0,
() => Outcomes.differentNames(role),
() => Outcomes.sameNames(role, byNames.values())
),
};
},
};
},
});

export namespace Outcomes {
export const differentNames = (role: Role.Name) =>
Ok.of(Diagnostic.of(`No two \`${role}\` have the same name.`));

export const sameNames = (
role: Role.Name,
errors: Iterable<Iterable<Element>>
) =>
Err.of(SameNames.of(`Some \`${role}\` have the same name.`, role, errors));
}

class SameNames extends Diagnostic implements Iterable<List<Element>> {
public static of(
message: string,
role: Role.Name = "none",
errors: Iterable<Iterable<Element>> = []
): SameNames {
return new SameNames(message, role, Array.from(errors).map(List.from));
}

private readonly _role: Role.Name;
private readonly _errors: ReadonlyArray<List<Element>>;

private constructor(
message: string,
role: Role.Name,
errors: ReadonlyArray<List<Element>>
) {
super(message);
this._role = role;
this._errors = errors;
}

public get role(): Role.Name {
return this._role;
}

public *[Symbol.iterator](): Iterator<List<Element>> {
yield* this._errors;
}

public equals(value: SameNames): boolean;

public equals(value: unknown): value is this;

public equals(value: unknown): boolean {
return (
value instanceof SameNames &&
value._message === this._message &&
value._role === this._role &&
value._errors.every((list, idx) => list.equals(this._errors[idx]))
);
}

public toJSON(): SameNames.JSON {
return {
...super.toJSON(),
role: this._role,
errors: Array.toJSON(this._errors),
};
}
}

namespace SameNames {
export interface JSON extends Diagnostic.JSON {
role: string;
errors: Array<List.JSON<Element>>;
}

export function isSameNames(value: unknown): value is SameNames {
return value instanceof SameNames;
}
}
17 changes: 8 additions & 9 deletions packages/alfa-rules/src/sia-r81/rule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,16 @@ import * as dom from "@siteimprove/alfa-dom";

import { expectation } from "../common/expectation";

import { hasNonEmptyAccessibleName } from "../common/predicate/has-non-empty-accessible-name";
import { hasRole } from "../common/predicate/has-role";
import { isIgnored } from "../common/predicate/is-ignored";
import {
hasNonEmptyAccessibleName,
hasRole,
isIgnored,
referenceSameResource,
} from "../common/predicate";

import { Question } from "../common/question";
import { Group } from "../common/group";
import { referenceSameResource } from "../common/predicate/reference-same-resource";
import { normalize } from "../common/normalize";
import { Question } from "../common/question";

const { isElement, hasName, hasNamespace, hasId } = Element;
const { flatten } = Iterable;
Expand Down Expand Up @@ -124,10 +127,6 @@ export namespace Outcomes {
);
}

function normalize(input: string): string {
return input.trim().toLowerCase().replace(/\s+/g, " ");
}

/**
* @todo For links in table cells, account for the text in the associated table
* header cell.
Expand Down
55 changes: 55 additions & 0 deletions packages/alfa-rules/test/sia-r56/rule.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { test } from "@siteimprove/alfa-test";

import { Document } from "@siteimprove/alfa-dom";

import R56, { Outcomes } from "../../src/sia-r56/rule";

import { evaluate } from "../common/evaluate";
import { passed, failed, inapplicable } from "../common/outcome";

import { Group } from "../../src/common/group";

test("evaluate() passes when same landmarks have different names", async (t) => {
const author = <aside aria-label="About the author" id="author" />;
const book = <aside aria-label="About the book" id="book" />;

const document = Document.of([author, book]);
const target = Group.of([author, book]);

t.deepEqual(await evaluate(R56, { document }), [
passed(R56, target, { 1: Outcomes.differentNames("complementary") }),
]);
});

test("evaluate() fails when same landmarks have same names", async (t) => {
const aside1 = <aside aria-label="More information" id="author" />;
const aside2 = <aside aria-label="More information" id="book" />;

const document = Document.of([aside1, aside2]);
const target = Group.of([aside1, aside2]);

t.deepEqual(await evaluate(R56, { document }), [
failed(R56, target, { 1: Outcomes.sameNames("complementary", [target]) }),
]);
});

test("evaluate() fails when same landmarks have no names", async (t) => {
const aside1 = <aside id="author" />;
const aside2 = <aside id="book" />;

const document = Document.of([aside1, aside2]);
const target = Group.of([aside1, aside2]);

t.deepEqual(await evaluate(R56, { document }), [
failed(R56, target, { 1: Outcomes.sameNames("complementary", [target]) }),
]);
});

test("evaluate() is inapplicable when only different landmarks exist", async (t) => {
const aside = <aside />;
const nav = <nav />;

const document = Document.of([aside, nav]);

t.deepEqual(await evaluate(R56, { document }), [inapplicable(R56)]);
});
3 changes: 3 additions & 0 deletions packages/alfa-rules/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"src/common/expectation/media-text-alternative.ts",
"src/common/expectation/video-description-track-accurate.ts",
"src/common/group.ts",
"src/common/normalize.ts",
"src/common/outcome/contrast.ts",
"src/common/outcome/text-spacing.ts",
"src/common/outcome/refresh-delay.ts",
Expand Down Expand Up @@ -111,6 +112,7 @@
"src/sia-r5/rule.ts",
"src/sia-r50/rule.ts",
"src/sia-r53/rule.ts",
"src/sia-r56/rule.ts",
"src/sia-r57/rule.ts",
"src/sia-r59/rule.ts",
"src/sia-r6/rule.ts",
Expand Down Expand Up @@ -181,6 +183,7 @@
"test/sia-r45/rule.spec.tsx",
"test/sia-r46/rule.spec.tsx",
"test/sia-r53/rule.spec.tsx",
"test/sia-r56/rule.spec.tsx",
"test/sia-r57/rule.spec.tsx",
"test/sia-r61/rule.spec.tsx",
"test/sia-r62/rule.spec.tsx",
Expand Down

0 comments on commit b743bc9

Please sign in to comment.