Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SIA-R8: add type="password" (and more) to applicability #1667

Merged
merged 32 commits into from
Sep 17, 2024
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
0b31288
adding failing unit test.
dan-tripp-siteimprove Aug 9, 2024
4b4afb9
adding more fialing tests.
dan-tripp-siteimprove Aug 10, 2024
d603a47
adding type="password" fields to the applicability. seems to work i.…
dan-tripp-siteimprove Aug 10, 2024
0b753b7
adding expecations Outcomes specifically for the input type="password…
dan-tripp-siteimprove Aug 10, 2024
27fbddc
- expanded applicability to include many more type="" attribute value…
dan-tripp-siteimprove Aug 13, 2024
f755a89
changeset.
dan-tripp-siteimprove Aug 27, 2024
2ce7ffe
changeset.
dan-tripp-siteimprove Aug 27, 2024
31c2a8e
Update packages/alfa-rules/src/sia-r8/rule.ts
dan-tripp-siteimprove Aug 30, 2024
983ecbd
Update packages/alfa-rules/test/sia-r8/rule.spec.tsx
dan-tripp-siteimprove Aug 30, 2024
a5f4959
Update packages/alfa-rules/test/sia-r8/rule.spec.tsx
dan-tripp-siteimprove Aug 30, 2024
c99988d
Update packages/alfa-rules/test/sia-r8/rule.spec.tsx
dan-tripp-siteimprove Aug 30, 2024
6c2a0a0
Update packages/alfa-rules/test/sia-r8/rule.spec.tsx
dan-tripp-siteimprove Aug 30, 2024
4cea3b7
Update packages/alfa-rules/test/sia-r8/rule.spec.tsx
dan-tripp-siteimprove Aug 30, 2024
a68a0c1
Update packages/alfa-rules/test/sia-r8/rule.spec.tsx
dan-tripp-siteimprove Aug 30, 2024
a1e18f5
Update packages/alfa-rules/test/sia-r8/rule.spec.tsx
dan-tripp-siteimprove Aug 30, 2024
159d693
fixing recently-introduced compilation error, I think.
dan-tripp-siteimprove Aug 30, 2024
b8c3fa1
fixing recently-introduced error in test.
dan-tripp-siteimprove Aug 30, 2024
f5aac10
changing import style as per review advice at https://github.com/Site…
dan-tripp-siteimprove Aug 30, 2024
7adf7cf
tightening up the types by using the same InputType type that hasInpu…
dan-tripp-siteimprove Aug 30, 2024
c0848bf
removing unused import.
dan-tripp-siteimprove Aug 30, 2024
af35fbc
Update packages/alfa-rules/test/sia-r8/rule.spec.tsx
dan-tripp-siteimprove Aug 30, 2024
861baeb
making new experimental rule: sia-er8.
dan-tripp-siteimprove Aug 30, 2024
dfc2719
changeset.
dan-tripp-siteimprove Aug 30, 2024
cedb192
refactoring as per review comment at https://github.com/Siteimprove/a…
dan-tripp-siteimprove Aug 31, 2024
7a40ee5
Extract API
github-actions[bot] Sep 2, 2024
d543582
Merge branch 'main' into r8-password-field
Jym77 Sep 2, 2024
5ee3fc1
Update .changeset/swift-meals-check.md
dan-tripp-siteimprove Sep 16, 2024
8b65244
Update packages/alfa-rules/src/experimental.ts
dan-tripp-siteimprove Sep 16, 2024
9b6bda3
Update packages/alfa-rules/src/sia-er8/rule.ts
dan-tripp-siteimprove Sep 16, 2024
64a9731
Update packages/alfa-rules/src/sia-er8/rule.ts
dan-tripp-siteimprove Sep 16, 2024
8613e4a
minor fixes - consequent to the review at https://github.com/Siteimpr…
dan-tripp-siteimprove Sep 16, 2024
f37faf4
Extract API
github-actions[bot] Sep 17, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/olive-seals-give.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@siteimprove/alfa-rules": minor
---

**Added:** R8: added type="password" to applicability. Also type="color", type="date", and more.
78 changes: 49 additions & 29 deletions packages/alfa-rules/src/sia-r8/rule.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Rule } from "@siteimprove/alfa-act";
import { Diagnostic, Rule } from "@siteimprove/alfa-act";
import type { Role } from "@siteimprove/alfa-aria";
import { DOM } from "@siteimprove/alfa-aria";
import * as aria from "@siteimprove/alfa-aria"
import { Element, Namespace, Node, Query } from "@siteimprove/alfa-dom";
import { Predicate } from "@siteimprove/alfa-predicate";
import { Err, Ok } from "@siteimprove/alfa-result";
Expand All @@ -12,10 +12,9 @@
import { Scope, Stability } from "../tags/index.js";
import { WithRole } from "../common/diagnostic.js";

const { hasNonEmptyAccessibleName, hasRole, isIncludedInTheAccessibilityTree } =
DOM;
const { hasNamespace } = Element;
const { and } = Predicate;
const { hasNonEmptyAccessibleName, hasRole, isIncludedInTheAccessibilityTree } = aria.DOM;
const { hasInputType, hasName, hasNamespace } = Element;
Fixed Show fixed Hide fixed
const { and, or } = Predicate;
const { getElementDescendants } = Query;

export default Rule.Atomic.of<Page, Element>({
Expand All @@ -28,34 +27,49 @@
return getElementDescendants(document, Node.fullTree).filter(
and(
hasNamespace(Namespace.HTML),
hasRole(
device,
"checkbox",
"combobox",
"listbox",
"menuitemcheckbox",
"menuitemradio",
"radio",
"searchbox",
"slider",
"spinbutton",
"switch",
"textbox",
or(
hasRole(
device,
"checkbox",
"combobox",
"listbox",
"menuitemcheckbox",
"menuitemradio",
"radio",
"searchbox",
"slider",
"spinbutton",
"switch",
"textbox",
),
hasInputType("password", "color", "date", "datetime-local", "file", "month", "time", "week"),
),
isIncludedInTheAccessibilityTree(device),
),
);
},

expectations(target) {
const role = WithRole.getRoleName(target, device);
return {
1: expectation(
hasNonEmptyAccessibleName(device)(target),
() => Outcomes.HasName(role),
() => Outcomes.HasNoName(role),
),
};
const role = aria.Node.from(target, device).role;
if(role.isSome()) {
const roleName = role.get().name;
return {
1: expectation(
hasNonEmptyAccessibleName(device)(target),
() => Outcomes.FormFieldWithAriaRoleHasName(roleName),
() => Outcomes.FormFieldWithAriaRoleHasNoName(roleName),
),
};
} else {
const type = target.attribute("type").map(attr => attr.value).getOr("");
return {
1: expectation(
hasNonEmptyAccessibleName(device)(target),
() => Outcomes.InputElementWithNoAriaRoleHasName(type),
() => Outcomes.InputElementWithNoAriaRoleHasNoName(type),
),
};
}
},
};
},
Expand All @@ -65,11 +79,17 @@
* @public
*/
export namespace Outcomes {
export const HasName = (role: Role.Name) =>
export const FormFieldWithAriaRoleHasName = (role: Role.Name) =>
Ok.of(WithRole.of(`The form field has an accessible name`, role));

export const HasNoName = (role: Role.Name) =>
export const FormFieldWithAriaRoleHasNoName = (role: Role.Name) =>
Err.of(
WithRole.of(`The form field does not have an accessible name`, role),
);

export const InputElementWithNoAriaRoleHasName = (typeAttribValue: string) =>
Ok.of(Diagnostic.of(`The type="${typeAttribValue}" form field has an accessible name`));

export const InputElementWithNoAriaRoleHasNoName = (typeAttribValue: string) =>
Err.of(Diagnostic.of(`The type="${typeAttribValue}" form field does not have an accessible name`));
dan-tripp-siteimprove marked this conversation as resolved.
Show resolved Hide resolved
}
173 changes: 163 additions & 10 deletions packages/alfa-rules/test/sia-r8/rule.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ test("evaluate() passes an input element with implicit label", async (t) => {

t.deepEqual(await evaluate(R8, { document }), [
passed(R8, target, {
1: Outcomes.HasName("textbox"),
1: Outcomes.FormFieldWithAriaRoleHasName("textbox"),
}),
]);
});
Expand All @@ -32,7 +32,7 @@ test("evaluate() passes an input element with aria-label", async (t) => {

t.deepEqual(await evaluate(R8, { document }), [
passed(R8, target, {
1: Outcomes.HasName("textbox"),
1: Outcomes.FormFieldWithAriaRoleHasName("textbox"),
}),
]);
});
Expand All @@ -53,7 +53,7 @@ test("evaluate() passes a select element with explicit label", async (t) => {

t.deepEqual(await evaluate(R8, { document }), [
passed(R8, target, {
1: Outcomes.HasName("listbox"),
1: Outcomes.FormFieldWithAriaRoleHasName("listbox"),
}),
]);
});
Expand All @@ -67,7 +67,7 @@ test("evaluate() passes a textarea element with aria-labelledby", async (t) => {

t.deepEqual(await evaluate(R8, { document }), [
passed(R8, target, {
1: Outcomes.HasName("textbox"),
1: Outcomes.FormFieldWithAriaRoleHasName("textbox"),
}),
]);
});
Expand All @@ -81,7 +81,7 @@ test("evaluate() passes a input element with placeholder attribute", async (t) =

t.deepEqual(await evaluate(R8, { document }), [
passed(R8, target, {
1: Outcomes.HasName("textbox"),
1: Outcomes.FormFieldWithAriaRoleHasName("textbox"),
}),
]);
});
Expand All @@ -100,7 +100,7 @@ test(`evaluate() passes a div element with explicit combobox role and an

t.deepEqual(await evaluate(R8, { document }), [
passed(R8, target, {
1: Outcomes.HasName(role),
1: Outcomes.FormFieldWithAriaRoleHasName(role),
}),
]);
});
Expand All @@ -112,7 +112,7 @@ test("evaluate() fails a input element without accessible name", async (t) => {

t.deepEqual(await evaluate(R8, { document }), [
failed(R8, target, {
1: Outcomes.HasNoName("textbox"),
1: Outcomes.FormFieldWithAriaRoleHasNoName("textbox"),
}),
]);
});
Expand All @@ -124,7 +124,7 @@ test("evaluate() fails a input element with empty aria-label", async (t) => {

t.deepEqual(await evaluate(R8, { document }), [
failed(R8, target, {
1: Outcomes.HasNoName("textbox"),
1: Outcomes.FormFieldWithAriaRoleHasNoName("textbox"),
}),
]);
});
Expand All @@ -143,7 +143,7 @@ test(`evaluate() fails a select element with aria-labelledby pointing to an

t.deepEqual(await evaluate(R8, { document }), [
failed(R8, target, {
1: Outcomes.HasNoName("listbox"),
1: Outcomes.FormFieldWithAriaRoleHasNoName("listbox"),
}),
]);
});
Expand All @@ -163,7 +163,7 @@ test("evaluate() fails a textbox with no accessible name", async (t) => {

t.deepEqual(await evaluate(R8, { document }), [
failed(R8, target, {
1: Outcomes.HasNoName(role),
1: Outcomes.FormFieldWithAriaRoleHasNoName(role),
}),
]);
});
Expand Down Expand Up @@ -197,3 +197,156 @@ test("evaluate() is inapplicable for an element which is not displayed", async (

t.deepEqual(await evaluate(R8, { document }), [inapplicable(R8)]);
});

test(`evaluate() fails an input element with type=password which is disabled
and without accessible name`, async (t) => {
const target = <input type="password" disabled/>;

const document = h.document([target]);

t.deepEqual(await evaluate(R8, { document }), [
failed(R8, target, {
1: Outcomes.InputElementWithNoAriaRoleHasNoName("password"),
}),
]);
});

test("evaluate() passes an input element with type=password and implicit label", async (t) => {
const target = <input type="password"/>;

const label = (
<label>
password
{target}
</label>
);

const document = h.document([label]);

t.deepEqual(await evaluate(R8, { document }), [
passed(R8, target, {
1: Outcomes.InputElementWithNoAriaRoleHasName("password"),
}),
]);
});

test("evaluate() passes an input element with type=password and aria-label", async (t) => {
const target = <input type="password" aria-label="password" disabled />;

const document = h.document([target]);

t.deepEqual(await evaluate(R8, { document }), [
passed(R8, target, {
1: Outcomes.InputElementWithNoAriaRoleHasName("password"),
}),
]);
});

test("evaluate() passes an input element with type=password and explicit label", async (t) => {
const target = <input type="password" id="country" />;

const label = <label for="country">Country</label>;
dan-tripp-siteimprove marked this conversation as resolved.
Show resolved Hide resolved

const document = h.document([label, target]);

t.deepEqual(await evaluate(R8, { document }), [
passed(R8, target, {
1: Outcomes.InputElementWithNoAriaRoleHasName("password"),
}),
]);
});

test("evaluate() passes an input element with type=password and aria-labelledby", async (t) => {
const target = <input type="password" aria-labelledby="pwd" />;

const label = <div id="pwd">Password</div>;

const document = h.document([label, target]);

t.deepEqual(await evaluate(R8, { document }), [
passed(R8, target, {
1: Outcomes.InputElementWithNoAriaRoleHasName("password"),
}),
]);
});

test("evaluate() passes an input element with type=password and placeholder attribute", async (t) => {
const target = <input type="password" placeholder="Enter your password" />;

const document = h.document([target]);

t.deepEqual(await evaluate(R8, { document }), [
passed(R8, target, {
1: Outcomes.InputElementWithNoAriaRoleHasName("password"),
}),
]);
});

test("evaluate() fails an input element with type=password and empty aria-label", async (t) => {
const target = <input type="password" aria-label=" " />;

const document = h.document([target]);

t.deepEqual(await evaluate(R8, { document }), [
failed(R8, target, {
1: Outcomes.InputElementWithNoAriaRoleHasNoName("password"),
}),
]);
});

test(`evaluate() fails an input element with type=password and aria-labelledby pointing to an
empty element`, async (t) => {
const target = <input type="password" aria-labelledby="country" />;

const label = <div id="country"></div>;

const document = h.document([label, target]);

t.deepEqual(await evaluate(R8, { document }), [
failed(R8, target, {
1: Outcomes.InputElementWithNoAriaRoleHasNoName("password"),
}),
]);
});

test(`evaluate() is inapplicable for an input element with type=password
and aria-hidden`, async (t) => {
const target = <input type="password" aria-hidden="true" aria-label="password" />;

const document = h.document([target]);

t.deepEqual(await evaluate(R8, { document }), [inapplicable(R8)]);
});

test(`evaluate() is inapplicable for an element with type=password and which
is not displayed`, async (t) => {
const target = <input type="password" aria-label="password" style={{ display: "none" }} />;

const document = h.document([target]);

t.deepEqual(await evaluate(R8, { document }), [inapplicable(R8)]);
});

test(`evaluate() fails for input elements with various types which give it no ARIA
role and which have no accessible name`, async (t) => {
for (const type of ["color", "date", "datetime-local", "file", "month", "time", "week"]) {
const target = <input type={type}/>;
const document = h.document([target])
t.deepEqual(await evaluate(R8, { document }),
[failed(R8, target, {1: Outcomes.InputElementWithNoAriaRoleHasNoName(type)})]);
}
});

test(`evaluate() passes for input elements with various types which give it no ARIA
role and which have an aria-label`, async (t) => {
const targets = [<input type="color" aria-label="x"/>, <input type="date" aria-label="x"/>,
<input type="datetime-local" aria-label="x"/>, <input type="file" aria-label="x"/>,
<input type="month" aria-label="x"/>, <input type="time" aria-label="x"/>,
<input type="week" aria-label="x"/>];
const document = h.document(targets)
t.deepEqual(await evaluate(R8, { document }),
targets.map(target => passed(R8, target, {
1: Outcomes.InputElementWithNoAriaRoleHasName(target.attribute("type")
.map(attr => attr.value).getOr("")) })),
);
});