From 170bb7121302aaeee58acaddfa01585e6ba3b52f Mon Sep 17 00:00:00 2001 From: Jean-Yves Moyen Date: Tue, 24 Aug 2021 16:21:04 +0200 Subject: [PATCH] Implement SIA-R70 (#870) * Copy implementation of R70 * Update documentation * Comments implemented * Update alfa-rules.api.md * Update rule.ts * Update alfa-rules.api.md Co-authored-by: elenamongelli <52746406+elenamongelli@users.noreply.github.com> --- docs/review/api/alfa-rules.api.md | 17 ++- packages/alfa-rules/src/rules.ts | 1 + packages/alfa-rules/src/sia-r70/rule.ts | 138 ++++++++++++++++++ .../alfa-rules/test/sia-r70/rule.spec.tsx | 107 ++++++++++++++ packages/alfa-rules/tsconfig.json | 2 + 5 files changed, 258 insertions(+), 7 deletions(-) create mode 100644 packages/alfa-rules/src/sia-r70/rule.ts create mode 100644 packages/alfa-rules/test/sia-r70/rule.spec.tsx diff --git a/docs/review/api/alfa-rules.api.md b/docs/review/api/alfa-rules.api.md index 728983a2b8..0a844a0db8 100644 --- a/docs/review/api/alfa-rules.api.md +++ b/docs/review/api/alfa-rules.api.md @@ -213,7 +213,7 @@ const _default_65: Rule.Atomic; const _default_66: Rule.Atomic; // @public (undocumented) -const _default_67: Rule.Atomic; +const _default_67: Rule.Atomic; // @public (undocumented) const _default_68: Rule.Atomic; @@ -237,16 +237,16 @@ const _default_72: Rule.Atomic; const _default_73: Rule.Atomic; // @public (undocumented) -const _default_74: Rule.Atomic, Question, Group>; +const _default_74: Rule.Atomic; // @public (undocumented) -const _default_75: Rule.Atomic; +const _default_75: Rule.Atomic, Question, Group>; // @public (undocumented) -const _default_76: Rule.Atomic; +const _default_76: Rule.Atomic; // @public (undocumented) -const _default_77: Rule.Atomic; +const _default_77: Rule.Atomic; // @public (undocumented) const _default_78: Rule.Atomic; @@ -258,10 +258,10 @@ const _default_79: Rule.Atomic; const _default_8: Rule.Atomic; // @public (undocumented) -const _default_80: Rule.Atomic; +const _default_80: Rule.Atomic; // @public (undocumented) -const _default_81: Rule.Atomic; +const _default_81: Rule.Atomic; // @public (undocumented) const _default_82: Rule.Atomic; @@ -281,6 +281,9 @@ const _default_86: Rule.Atomic; // @public (undocumented) const _default_87: Rule.Atomic; +// @public (undocumented) +const _default_88: Rule.Atomic; + // @public (undocumented) const _default_9: Rule.Atomic; diff --git a/packages/alfa-rules/src/rules.ts b/packages/alfa-rules/src/rules.ts index 3ebade7b63..0a60e489a8 100644 --- a/packages/alfa-rules/src/rules.ts +++ b/packages/alfa-rules/src/rules.ts @@ -64,6 +64,7 @@ export { default as R66 } from "./sia-r66/rule"; export { default as R67 } from "./sia-r67/rule"; export { default as R68 } from "./sia-r68/rule"; export { default as R69 } from "./sia-r69/rule"; +export { default as R70 } from "./sia-r70/rule"; export { default as R71 } from "./sia-r71/rule"; export { default as R72 } from "./sia-r72/rule"; export { default as R73 } from "./sia-r73/rule"; diff --git a/packages/alfa-rules/src/sia-r70/rule.ts b/packages/alfa-rules/src/sia-r70/rule.ts new file mode 100644 index 0000000000..5631e71d9c --- /dev/null +++ b/packages/alfa-rules/src/sia-r70/rule.ts @@ -0,0 +1,138 @@ +import { Rule, Diagnostic } from "@siteimprove/alfa-act"; +import { Array } from "@siteimprove/alfa-array"; +import { Document, Element, Namespace } from "@siteimprove/alfa-dom"; +import { Iterable } from "@siteimprove/alfa-iterable"; +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 { hasChild, isRendered, isDocumentElement } from "../common/predicate"; + +const { isElement, hasName, hasNamespace } = Element; +const { and, test } = Predicate; + +const isDeprecated = hasName( + "acronym", + "applet", + "basefont", + "bgsound", + "big", + "blink", + "center", + "content", + "dir", + "font", + "frame", + "frameset", + "hgroup", + "image", + "keygen", + "marquee", + "menuitem", + "nobr", + "noembed", + "noframes", + "plaintext", + "rb", + "rtc", + "shadow", + "spacer", + "strike", + "tt", + "xmp" +); + +export default Rule.Atomic.of({ + uri: "https://alfa.siteimprove.com/rules/sia-r70", + evaluate({ device, document }) { + return { + applicability() { + return test(hasChild(isDocumentElement), document) ? [document] : []; + }, + + expectations(target) { + const deprecatedElements = target + .descendants({ flattened: true, nested: true }) + .filter(isElement) + .filter( + and(hasNamespace(Namespace.HTML), isDeprecated, isRendered(device)) + ); + + return { + 1: expectation( + deprecatedElements.isEmpty(), + () => Outcomes.HasNoDeprecatedElement, + () => Outcomes.HasDeprecatedElements(deprecatedElements) + ), + }; + }, + }; + }, +}); + +export namespace Outcomes { + export const HasNoDeprecatedElement = Ok.of( + Diagnostic.of(`The document doesn't contain any deprecated elements`) + ); + + export const HasDeprecatedElements = (errors: Iterable) => + Err.of( + DeprecatedElements.of(`The document contains deprecated elements`, errors) + ); +} + +class DeprecatedElements extends Diagnostic implements Iterable { + public static of( + message: string, + errors: Iterable = [] + ): DeprecatedElements { + return new DeprecatedElements(message, Array.from(errors)); + } + + private _errors: ReadonlyArray; + + private constructor(message: string, errors: Array) { + super(message); + this._errors = errors; + } + + public get errors(): ReadonlyArray { + return this._errors; + } + + public equals(value: DeprecatedElements): boolean; + + public equals(value: unknown): value is this; + + public equals(value: unknown): boolean { + return ( + value instanceof DeprecatedElements && + value._message === this._message && + Array.equals(value._errors, this._errors) + ); + } + + public *[Symbol.iterator](): Iterator { + yield* this._errors; + } + + public toJSON(): DeprecatedElements.JSON { + return { + ...super.toJSON(), + errors: this._errors.map((element) => element.path()), + }; + } +} + +namespace DeprecatedElements { + export interface JSON extends Diagnostic.JSON { + errors: Array; + } + + export function isDeprecatedElements( + value: unknown + ): value is DeprecatedElements { + return value instanceof DeprecatedElements; + } +} diff --git a/packages/alfa-rules/test/sia-r70/rule.spec.tsx b/packages/alfa-rules/test/sia-r70/rule.spec.tsx new file mode 100644 index 0000000000..f4b85098cc --- /dev/null +++ b/packages/alfa-rules/test/sia-r70/rule.spec.tsx @@ -0,0 +1,107 @@ +import { h } from "@siteimprove/alfa-dom"; +import { test } from "@siteimprove/alfa-test"; + +import R70, { Outcomes } from "../../src/sia-r70/rule"; + +import { evaluate } from "../common/evaluate"; +import { passed, failed, inapplicable } from "../common/outcome"; + +test("evaluate() passes a page with no deprecated / obsolete elements ", async (t) => { + const target = ( + +

Lorem ipsum.

+ + ); + + const document = h.document([target]); + + t.deepEqual(await evaluate(R70, { document }), [ + passed(R70, document, { + 1: Outcomes.HasNoDeprecatedElement, + }), + ]); +}); + +test("evaluate() passes a page with a deprecated but not rendered element", async (t) => { + const target = ( + +

+ Lorem +

+ + ); + + const document = h.document([target]); + + t.deepEqual(await evaluate(R70, { document }), [ + passed(R70, document, { + 1: Outcomes.HasNoDeprecatedElement, + }), + ]); +}); + +test("evaluate() fails a page with deprecated and rendered element", async (t) => { + const blink = not; + const target = ( + +

Schrödinger's cat is {blink} dead.

+ + ); + + const document = h.document([target]); + + t.deepEqual(await evaluate(R70, { document }), [ + failed(R70, document, { + 1: Outcomes.HasDeprecatedElements([blink]), + }), + ]); +}); + +test("evaluate() fails a page with deprecated visible element", async (t) => { + const blink = ; + const target = ( + +

Schrödinger's cat is {blink} dead.

+ + ); + + const document = h.document([target]); + + t.deepEqual(await evaluate(R70, { document }), [ + failed(R70, document, { + 1: Outcomes.HasDeprecatedElements([blink]), + }), + ]); +}); + +test("evaluate() fails a page with two deprecated elements in the accessibility tree", async (t) => { + const menuitem1 = Foo; + const menuitem2 = Bar; + const target = ( + +
    + {menuitem1} + {menuitem2} +
+ + ); + + const document = h.document([target]); + + t.deepEqual(await evaluate(R70, { document }), [ + failed(R70, document, { + 1: Outcomes.HasDeprecatedElements([menuitem1, menuitem2]), + }), + ]); +}); + +test("evaluate() is inapplicable to non-HTML documents", async (t) => { + const document = h.document([ + + This is a circle + + , + ]); + + t.deepEqual(await evaluate(R70, { document }), [inapplicable(R70)]); +}); diff --git a/packages/alfa-rules/tsconfig.json b/packages/alfa-rules/tsconfig.json index 5b92a00f95..5f6cd7ab19 100644 --- a/packages/alfa-rules/tsconfig.json +++ b/packages/alfa-rules/tsconfig.json @@ -136,6 +136,7 @@ "src/sia-r67/rule.ts", "src/sia-r68/rule.ts", "src/sia-r69/rule.ts", + "src/sia-r70/rule.ts", "src/sia-r7/rule.ts", "src/sia-r71/rule.ts", "src/sia-r72/rule.ts", @@ -212,6 +213,7 @@ "test/sia-r67/rule.spec.tsx", "test/sia-r68/rule.spec.tsx", "test/sia-r69/rule.spec.tsx", + "test/sia-r70/rule.spec.tsx", "test/sia-r71/rule.spec.tsx", "test/sia-r72/rule.spec.tsx", "test/sia-r73/rule.spec.tsx",