fix(a11y): describe protected administrator actions without replacing names
This commit is contained in:
@@ -0,0 +1,11 @@
|
|||||||
|
# Last-administrator help semantics
|
||||||
|
|
||||||
|
The combined fixture review retained28axe scans with0violations and2155incomplete node observations. Most are overlapping/obscured Audit-table contrast contexts at the full fixture's larger row count; ten prohibited-name observations include roleless disabled-action Tooltip wrappers and MUI DataGrid Sort spans. These are triaged contexts, not a claim of human acceptance or2155confirmed defects.
|
||||||
|
|
||||||
|
The confirmed owner correction applies the existing `describeChild` pattern to the four last-administrator help Tooltips on mobile cards and the desktop grid. A roleless wrapper no longer receives an accessible name; the disabled Remove admin/Delete buttons retain their own names, and their unchanged help remains available. No permission, disabled condition, confirmation, API payload or data operation changes.
|
||||||
|
|
||||||
|
The existing final-administrator component case now asserts that the wrapper has no `aria-label` and that hovering still reveals the same help text. It fails before the correction (9PASS/1FAIL) and passes afterward (10/10). No case is added or assertion removed. Full frontend inventory remains899/101.
|
||||||
|
|
||||||
|
[Portable evidence](evidence/admin-help.json) records the10component cases, all3native Users cases with zero skips/flaky/retries, and7all-enabled axe scans with0violations.21contrast incomplete nodes remain in overlap/obscured/short-badge contexts; four prohibited-name observations are MUI DataGrid Sort spans. The final-administrator wrapper names are no longer observed. Their raw contexts remain retained; no rule or assertion is disabled.
|
||||||
|
|
||||||
|
Fresh production build, application/browser TypeScript and lint pass. The native run includes actual final-admin409, ordinary-user403, persisted role/session changes, disabled deletion503, GET-only recovery and bilingual mobile/desktop widths. Native viewport/keyboard evidence is not human screen-reader, physical touch, real zoom or production acceptance. Whole integrated successor execution remains P7-12's final gate. PRES-A01/E08/E09 and all57group dispositions remain; no Phase8 implementation.
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"checkpoint": "p7-12",
|
||||||
|
"status": "PASS",
|
||||||
|
"checkedUtc": "2026-10-21T05:22:54.393478+00:00",
|
||||||
|
"commands": [
|
||||||
|
{
|
||||||
|
"name": "secret-scan",
|
||||||
|
"command": [
|
||||||
|
"rtk",
|
||||||
|
"proxy",
|
||||||
|
"C:\\Users\\Connor\\AppData\\Local\\Programs\\Python\\Python312\\python.exe",
|
||||||
|
"-X",
|
||||||
|
"utf8",
|
||||||
|
"scripts/supply-chain.py",
|
||||||
|
"scan-secrets"
|
||||||
|
],
|
||||||
|
"startedUtc": "2026-10-21T05:22:50.908124+00:00",
|
||||||
|
"finishedUtc": "2026-10-21T05:22:53.599465+00:00",
|
||||||
|
"exitCode": 0,
|
||||||
|
"log": ".artifacts/phase7/current/static/20261021T052250Z/secret-scan.log",
|
||||||
|
"logSha256Raw": "be5119e6057287d397e87759f855c91e634e3bfac61c8be7584d05592ba2dfcd"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "working-whitespace",
|
||||||
|
"command": [
|
||||||
|
"rtk",
|
||||||
|
"proxy",
|
||||||
|
"git",
|
||||||
|
"diff",
|
||||||
|
"--check"
|
||||||
|
],
|
||||||
|
"startedUtc": "2026-10-21T05:22:53.599948+00:00",
|
||||||
|
"finishedUtc": "2026-10-21T05:22:53.686928+00:00",
|
||||||
|
"exitCode": 0,
|
||||||
|
"log": ".artifacts/phase7/current/static/20261021T052250Z/working-whitespace.log",
|
||||||
|
"logSha256Raw": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "staged-whitespace",
|
||||||
|
"command": [
|
||||||
|
"rtk",
|
||||||
|
"proxy",
|
||||||
|
"git",
|
||||||
|
"diff",
|
||||||
|
"--cached",
|
||||||
|
"--check"
|
||||||
|
],
|
||||||
|
"startedUtc": "2026-10-21T05:22:53.687422+00:00",
|
||||||
|
"finishedUtc": "2026-10-21T05:22:53.776484+00:00",
|
||||||
|
"exitCode": 0,
|
||||||
|
"log": ".artifacts/phase7/current/static/20261021T052250Z/staged-whitespace.log",
|
||||||
|
"logSha256Raw": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "phase7-whitespace",
|
||||||
|
"command": [
|
||||||
|
"rtk",
|
||||||
|
"proxy",
|
||||||
|
"git",
|
||||||
|
"diff",
|
||||||
|
"--check",
|
||||||
|
"b5319981ad3d1bd635daff861453d23c0de5320b"
|
||||||
|
],
|
||||||
|
"startedUtc": "2026-10-21T05:22:53.776984+00:00",
|
||||||
|
"finishedUtc": "2026-10-21T05:22:53.975095+00:00",
|
||||||
|
"exitCode": 0,
|
||||||
|
"log": ".artifacts/phase7/current/static/20261021T052250Z/phase7-whitespace.log",
|
||||||
|
"logSha256Raw": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"untrackedSecretScan": {
|
||||||
|
"files": 0,
|
||||||
|
"findings": 0
|
||||||
|
},
|
||||||
|
"documentation": {
|
||||||
|
"files": 43,
|
||||||
|
"relativeLinks": 669,
|
||||||
|
"errors": []
|
||||||
|
},
|
||||||
|
"preservation": {
|
||||||
|
"groups": 57,
|
||||||
|
"statuses": {
|
||||||
|
"PARTIAL": 56,
|
||||||
|
"PASS": 0,
|
||||||
|
"FAIL": 0,
|
||||||
|
"DEFERRED": 1
|
||||||
|
},
|
||||||
|
"originalRoutesPreserved": true,
|
||||||
|
"currentConcreteRoutes": 33
|
||||||
|
},
|
||||||
|
"production": {
|
||||||
|
"main": "8699ae4f421bb1d049b937e735e40a4797a4b719",
|
||||||
|
"originMain": "8699ae4f421bb1d049b937e735e40a4797a4b719",
|
||||||
|
"status": "NO-GO",
|
||||||
|
"qualification": "Local fetched refs; direct push observation recorded separately."
|
||||||
|
},
|
||||||
|
"hashPolicy": "Portable JSON UTF8 LF; raw log hashes measure exact saved bytes.",
|
||||||
|
"limits": [
|
||||||
|
"Pattern scanner skips binary and files over2MiB; not historical Git/production secrets.",
|
||||||
|
"Local links check target existence, not URL/heading anchors.",
|
||||||
|
"Static evidence is not runtime/manual/device/provider/production acceptance."
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
{
|
||||||
|
"observedUtc": "2026-10-21T05:22:23.564754+00:00",
|
||||||
|
"scope": "Existing last-administrator disabled-action Tooltip wrappers on mobile cards and desktop grid use describeChild. Labels, disabled state, help text, confirmation, server admission and writes are unchanged. Existing component case strengthened; zero added cases.",
|
||||||
|
"unit": {
|
||||||
|
"p712-admin-tooltip-red": {
|
||||||
|
"path": ".artifacts/phase7/current/unit/p712-admin-tooltip-red-20261021T051907Z-command.json",
|
||||||
|
"sha256": "77ce3fccbfabece87bfe68d7404af3caa5728a5f542ecaedf942a6493c476ec5",
|
||||||
|
"command": [
|
||||||
|
"rtk",
|
||||||
|
"proxy",
|
||||||
|
"npm.cmd",
|
||||||
|
"test",
|
||||||
|
"--",
|
||||||
|
"--runInBand",
|
||||||
|
"--watchAll=false",
|
||||||
|
"--json",
|
||||||
|
"--outputFile=E:\\Storage\\Projects\\Active\\Job tracker\\.artifacts\\phase7\\current\\unit\\p712-admin-tooltip-red-20261021T051907Z.json",
|
||||||
|
"src/admin-users-page.test.tsx"
|
||||||
|
],
|
||||||
|
"startedUtc": "2026-10-21T05:19:07.132180+00:00",
|
||||||
|
"finishedUtc": "2026-10-21T05:19:14.112556+00:00",
|
||||||
|
"exitCode": 1,
|
||||||
|
"counts": {
|
||||||
|
"numPassedTests": 9,
|
||||||
|
"numFailedTests": 1,
|
||||||
|
"numPendingTests": 0,
|
||||||
|
"numTodoTests": 0,
|
||||||
|
"numPassedTestSuites": 0,
|
||||||
|
"numFailedTestSuites": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"p712-admin-tooltip-green": {
|
||||||
|
"path": ".artifacts/phase7/current/unit/p712-admin-tooltip-green-20261021T051928Z-command.json",
|
||||||
|
"sha256": "e8863431c5dcec50c1ea1085a97753b98d3384ebd1abea45eb3690505d3b97fb",
|
||||||
|
"command": [
|
||||||
|
"rtk",
|
||||||
|
"proxy",
|
||||||
|
"npm.cmd",
|
||||||
|
"test",
|
||||||
|
"--",
|
||||||
|
"--runInBand",
|
||||||
|
"--watchAll=false",
|
||||||
|
"--json",
|
||||||
|
"--outputFile=E:\\Storage\\Projects\\Active\\Job tracker\\.artifacts\\phase7\\current\\unit\\p712-admin-tooltip-green-20261021T051928Z.json",
|
||||||
|
"src/admin-users-page.test.tsx"
|
||||||
|
],
|
||||||
|
"startedUtc": "2026-10-21T05:19:28.259530+00:00",
|
||||||
|
"finishedUtc": "2026-10-21T05:19:33.800673+00:00",
|
||||||
|
"exitCode": 0,
|
||||||
|
"counts": {
|
||||||
|
"numPassedTests": 10,
|
||||||
|
"numFailedTests": 0,
|
||||||
|
"numPendingTests": 0,
|
||||||
|
"numTodoTests": 0,
|
||||||
|
"numPassedTestSuites": 1,
|
||||||
|
"numFailedTestSuites": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"native": {
|
||||||
|
"stats": {
|
||||||
|
"startTime": "2026-10-21T05:20:10.074Z",
|
||||||
|
"duration": 25335.5,
|
||||||
|
"expected": 3,
|
||||||
|
"skipped": 0,
|
||||||
|
"unexpected": 0,
|
||||||
|
"flaky": 0
|
||||||
|
},
|
||||||
|
"path": ".artifacts/phase7/current/ui/admin-users.json",
|
||||||
|
"sha256": "e711f9b03ec133d48b3ec9b33cc86f9bf507fc0477475bf34e6bd0ead25fc963",
|
||||||
|
"command": {
|
||||||
|
"name": "admin-users",
|
||||||
|
"command": [
|
||||||
|
"rtk",
|
||||||
|
"proxy",
|
||||||
|
"node",
|
||||||
|
"node_modules/@playwright/test/cli.js",
|
||||||
|
"test",
|
||||||
|
"e2e/phase7-admin-users.spec.ts",
|
||||||
|
"--retries=0",
|
||||||
|
"--reporter=list,json",
|
||||||
|
"--output=E:\\Storage\\Projects\\Active\\Job tracker\\.artifacts\\phase7\\current\\ui\\admin-users-results"
|
||||||
|
],
|
||||||
|
"startedUtc": "2026-10-21T05:20:09.392563+00:00",
|
||||||
|
"finishedUtc": "2026-10-21T05:20:35.451870+00:00",
|
||||||
|
"exitCode": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sources": {
|
||||||
|
"job-tracker-ui/src/views/AdminUsersPage.tsx": "c015f87184fb9ab7e66303d9c293214d5fe249013954030ca64da48d657a8bd2",
|
||||||
|
"job-tracker-ui/src/admin-users-page.test.tsx": "a148723fe696cea93dcd4cf5c59cd5f7654d94c34cfdd25f7e30b2915ad8ef1a"
|
||||||
|
},
|
||||||
|
"axe": {
|
||||||
|
"scans": 7,
|
||||||
|
"violations": 0,
|
||||||
|
"incompleteNodes": {
|
||||||
|
"color-contrast": 21,
|
||||||
|
"aria-prohibited-attr": 4
|
||||||
|
},
|
||||||
|
"remainingContexts": [
|
||||||
|
"<span aria-label=\"Sort\" class=\"\">",
|
||||||
|
"<span aria-label=\"Sort\" class=\"\">",
|
||||||
|
"<span aria-label=\"Sort\" class=\"\">",
|
||||||
|
"<span aria-label=\"Sort\" class=\"\">"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"qualification": "7 all-enabled axe scans have0violations;21 contrast incomplete nodes are obscured/overlap/short badge contexts and4 prohibited-name observations are MUI DataGrid Sort spans. Raw contexts remain for settled/manual triage. Final-administrator wrapper prohibited names are no longer observed. No human reader/device acceptance or new visual styling claim."
|
||||||
|
}
|
||||||
@@ -115,7 +115,11 @@ test("warns before demoting another administrator", async () => {
|
|||||||
test("disables demotion and deletion for the final administrator", async () => {
|
test("disables demotion and deletion for the final administrator", async () => {
|
||||||
renderPage([{ id: "me", email: "me@example.com", userName: "me", roles: ["Admin"], emailConfirmed: true, isCurrentUser: true, canRemoveAdmin: false }]);
|
renderPage([{ id: "me", email: "me@example.com", userName: "me", roles: ["Admin"], emailConfirmed: true, isCurrentUser: true, canRemoveAdmin: false }]);
|
||||||
|
|
||||||
expect(await screen.findByRole("button", { name: "Remove admin" })).toBeDisabled();
|
const demote = await screen.findByRole("button", { name: "Remove admin" });
|
||||||
|
expect(demote).toBeDisabled();
|
||||||
|
expect(demote.parentElement).not.toHaveAttribute("aria-label");
|
||||||
|
fireEvent.mouseOver(demote.parentElement!);
|
||||||
|
expect(await screen.findByRole("tooltip")).toHaveTextContent("The final administrator cannot be demoted or deleted.");
|
||||||
expect(screen.getByRole("button", { name: "Delete" })).toBeDisabled();
|
expect(screen.getByRole("button", { name: "Delete" })).toBeDisabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -200,7 +200,7 @@ export default function AdminUsersPage() {
|
|||||||
const isAdmin = (user.roles || []).includes("Admin");
|
const isAdmin = (user.roles || []).includes("Admin");
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap", py: 0.5 }}>
|
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap", py: 0.5 }}>
|
||||||
<Tooltip title={isAdmin && !user.canRemoveAdmin ? t("adminUsersLastAdminHelp") : ""}>
|
<Tooltip describeChild title={isAdmin && !user.canRemoveAdmin ? t("adminUsersLastAdminHelp") : ""}>
|
||||||
<span>
|
<span>
|
||||||
<Button size="small" variant={isAdmin ? "contained" : "outlined"} disabled={busy || isAdmin && !user.canRemoveAdmin} onClick={() => void setAdminRole(user, !isAdmin)}>
|
<Button size="small" variant={isAdmin ? "contained" : "outlined"} disabled={busy || isAdmin && !user.canRemoveAdmin} onClick={() => void setAdminRole(user, !isAdmin)}>
|
||||||
{isAdmin ? t("adminUsersRemoveAdmin") : t("adminUsersMakeAdmin")}
|
{isAdmin ? t("adminUsersRemoveAdmin") : t("adminUsersMakeAdmin")}
|
||||||
@@ -210,7 +210,7 @@ export default function AdminUsersPage() {
|
|||||||
<Button size="small" variant="outlined" disabled={busy} onClick={() => void sendReset(user)}>
|
<Button size="small" variant="outlined" disabled={busy} onClick={() => void sendReset(user)}>
|
||||||
{t("adminUsersSendReset")}
|
{t("adminUsersSendReset")}
|
||||||
</Button>
|
</Button>
|
||||||
<Tooltip title={isAdmin && !user.canRemoveAdmin ? t("adminUsersLastAdminHelp") : ""}>
|
<Tooltip describeChild title={isAdmin && !user.canRemoveAdmin ? t("adminUsersLastAdminHelp") : ""}>
|
||||||
<span><Button size="small" color="error" variant="outlined" disabled={busy || isAdmin && !user.canRemoveAdmin} onClick={() => void remove(user)}>{t("adminUsersDelete")}</Button></span>
|
<span><Button size="small" color="error" variant="outlined" disabled={busy || isAdmin && !user.canRemoveAdmin} onClick={() => void remove(user)}>{t("adminUsersDelete")}</Button></span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -275,13 +275,13 @@ export default function AdminUsersPage() {
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Stack spacing={1}>
|
<Stack spacing={1}>
|
||||||
<Tooltip title={isAdmin && !user.canRemoveAdmin ? t("adminUsersLastAdminHelp") : ""}>
|
<Tooltip describeChild title={isAdmin && !user.canRemoveAdmin ? t("adminUsersLastAdminHelp") : ""}>
|
||||||
<span><Button variant={isAdmin ? "contained" : "outlined"} disabled={busy || isAdmin && !user.canRemoveAdmin} onClick={() => void setAdminRole(user, !isAdmin)} fullWidth>{isAdmin ? t("adminUsersRemoveAdmin") : t("adminUsersMakeAdmin")}</Button></span>
|
<span><Button variant={isAdmin ? "contained" : "outlined"} disabled={busy || isAdmin && !user.canRemoveAdmin} onClick={() => void setAdminRole(user, !isAdmin)} fullWidth>{isAdmin ? t("adminUsersRemoveAdmin") : t("adminUsersMakeAdmin")}</Button></span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Button variant="outlined" disabled={busy} onClick={() => void sendReset(user)} fullWidth>
|
<Button variant="outlined" disabled={busy} onClick={() => void sendReset(user)} fullWidth>
|
||||||
{t("adminUsersSendReset")}
|
{t("adminUsersSendReset")}
|
||||||
</Button>
|
</Button>
|
||||||
<Tooltip title={isAdmin && !user.canRemoveAdmin ? t("adminUsersLastAdminHelp") : ""}>
|
<Tooltip describeChild title={isAdmin && !user.canRemoveAdmin ? t("adminUsersLastAdminHelp") : ""}>
|
||||||
<span><Button color="error" variant="outlined" disabled={busy || isAdmin && !user.canRemoveAdmin} onClick={() => void remove(user)} fullWidth>{t("adminUsersDelete")}</Button></span>
|
<span><Button color="error" variant="outlined" disabled={busy || isAdmin && !user.canRemoveAdmin} onClick={() => void remove(user)} fullWidth>{t("adminUsersDelete")}</Button></span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
Reference in New Issue
Block a user