diff --git a/clients/web/src/components/groups/ServerSettingsForm/ServerSettingsForm.test.tsx b/clients/web/src/components/groups/ServerSettingsForm/ServerSettingsForm.test.tsx index b66d76ac..a9ff4f5d 100644 --- a/clients/web/src/components/groups/ServerSettingsForm/ServerSettingsForm.test.tsx +++ b/clients/web/src/components/groups/ServerSettingsForm/ServerSettingsForm.test.tsx @@ -826,6 +826,83 @@ describe("ServerSettingsForm", () => { ); }); + // #2018 — the section heading and the placeholders are not programmatically + // associated with these inputs, so each control carries its own aria-label and + // the reserved-key reason is the input's `error` string (which Mantine wires + // to the input via aria-describedby) rather than free-standing text. + it("gives each authorization-parameter control an accessible name", () => { + renderWithMantine( + , + ); + expect( + screen.getByRole("textbox", { + name: "Authorization parameter name, kc_idp_hint", + }), + ).toHaveValue("kc_idp_hint"); + expect( + screen.getByRole("textbox", { + name: "Authorization parameter value, kc_idp_hint", + }), + ).toHaveValue("corp"); + expect( + screen.getByRole("button", { + name: "Remove authorization parameter kc_idp_hint", + }), + ).toBeInTheDocument(); + }); + + it("falls back to a row number in the accessible name when the key is blank", () => { + renderWithMantine( + , + ); + expect( + screen.getByRole("textbox", { + name: "Authorization parameter name, row 1", + }), + ).toBeInTheDocument(); + }); + + // The reason must reach assistive tech through the input, not just as text on + // the page: Mantine emits the `error` string with an id the input references. + it("associates the reserved-key reason with the key input", () => { + renderWithMantine( + , + ); + const keyInput = screen.getByRole("textbox", { + name: "Authorization parameter name, state", + }); + const describedBy = keyInput.getAttribute("aria-describedby"); + expect(describedBy).toBeTruthy(); + const reason = describedBy + ?.split(" ") + .map((id) => document.getElementById(id)?.textContent ?? "") + .join(" "); + expect(reason).toContain( + '"state" is set by the authorization flow and cannot be overridden.', + ); + }); + it("does not flag a blank authorization-parameter row as reserved", () => { renderWithMantine( - {params.map((param, index) => ( - - + {params.map((param, index) => { + const rowLabel = param.key.trim() || `row ${index + 1}`; + return ( + onChange(index, e.currentTarget.value, param.value) } @@ -301,6 +306,7 @@ function AuthorizationParamRows({ } /> @@ -312,15 +318,15 @@ function AuthorizationParamRows({ ) : null } /> - onRemove(index)}>X + onRemove(index)} + > + X + - {authorizationParamKeyError(param.key) ? ( - - {authorizationParamKeyError(param.key)} - - ) : null} - - ))} + ); + })} ); } diff --git a/clients/web/src/test/integration/mcp/remote/servers-route.test.ts b/clients/web/src/test/integration/mcp/remote/servers-route.test.ts index 9e0f8a5a..18803b5a 100644 --- a/clients/web/src/test/integration/mcp/remote/servers-route.test.ts +++ b/clients/web/src/test/integration/mcp/remote/servers-route.test.ts @@ -471,9 +471,8 @@ describe("/api/servers routes", () => { }), }); expect(res.status).toBe(200); - const stored = readConfig(h.configPath).mcpServers - .authparams as unknown as Record; - expect(stored.oauth).toEqual({ + const stored = readConfig(h.configPath).mcpServers.authparams; + expect(stored?.oauth).toEqual({ authorizationParams: { kc_idp_hint: "corp" }, }); });