admin: associate inline field errors with their inputs
network assignment rows, activity filters, and the settings password pair now mark the offending input with aria-invalid and point it at the error text with aria-describedby. the prefix validator returns the row and field it is about, and any row mutation clears a message that named a position. settings numeric fields carry aria-invalid on an unparseable value.
This commit is contained in:
@@ -176,12 +176,52 @@ test("enum and boolean fields diff as their own types", async () => {
|
||||
expect(putBodies[0]).toEqual({ logging: { level: "debug", hide_domains: true } });
|
||||
});
|
||||
|
||||
/** The text of the elements an input points at with `aria-describedby`. */
|
||||
function describedText(input: HTMLElement): string {
|
||||
const ids = input.getAttribute("aria-describedby");
|
||||
if (ids === null) throw new Error("input has no aria-describedby");
|
||||
return ids
|
||||
.split(/\s+/)
|
||||
.map((id) => {
|
||||
const node = document.getElementById(id);
|
||||
if (node === null) throw new Error(`aria-describedby names missing element ${id}`);
|
||||
return node.textContent ?? "";
|
||||
})
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
test("clearing a number field disables Save instead of sending NaN", async () => {
|
||||
await openSystem();
|
||||
|
||||
const cache = screen.getByRole("group", { name: "Cache" });
|
||||
fireEvent.change(within(cache).getByLabelText("size"), { target: { value: "" } });
|
||||
const size = within(cache).getByLabelText("size");
|
||||
fireEvent.change(size, { target: { value: "" } });
|
||||
expect(saveButton().disabled).toBe(true);
|
||||
// The refusal is on the field itself, not only on the Save button.
|
||||
expect(size.getAttribute("aria-invalid")).toBe("true");
|
||||
|
||||
fireEvent.change(size, { target: { value: "512" } });
|
||||
expect(size.getAttribute("aria-invalid")).toBeNull();
|
||||
});
|
||||
|
||||
test("the mismatch message is attached to both password inputs", async () => {
|
||||
await openSystem();
|
||||
|
||||
const web = screen.getByRole("group", { name: "Web" });
|
||||
const passwordInput = within(web).getByLabelText("password");
|
||||
const confirmInput = within(web).getByLabelText("confirm password");
|
||||
|
||||
fireEvent.change(passwordInput, { target: { value: "hunter2" } });
|
||||
for (const input of [passwordInput, confirmInput]) {
|
||||
expect(input.getAttribute("aria-invalid")).toBe("true");
|
||||
expect(describedText(input)).toBe("Passwords do not match.");
|
||||
}
|
||||
|
||||
fireEvent.change(confirmInput, { target: { value: "hunter2" } });
|
||||
for (const input of [passwordInput, confirmInput]) {
|
||||
expect(input.getAttribute("aria-invalid")).toBeNull();
|
||||
expect(input.getAttribute("aria-describedby")).toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
test("password flow: note shown, confirm required, PUT sends web.password, no restart notice", async () => {
|
||||
|
||||
Reference in New Issue
Block a user