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" },
});
});