fix(web): lock header rows while submitting, name their clear buttons (#1915)

Round-2 review follow-ups on #2038, all three from the suppressed set.

The header rows stayed editable while a save was in flight, unlike the rest
of the modal — a slow request let a user edit a row whose value had already
been captured, and the modal then closed having persisted the earlier one.
`KeyValueRows` takes a `disabled` prop and the modal passes `submitting`.

Each row's clear buttons were still named the bare "Clear", so a screen
reader saw six indistinguishable buttons across three rows. They now name
the field they empty ("Clear header name, Cookie").

The App-level headers-into-settings merge — the seam that distinguishes an
edit from a clone, and where the credential copy lived — moves into
`utils/serverSettingsPatch.ts` with its own tests. App.tsx is outside the
coverage gate, so inline it could not be covered at all.

Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-08-16 21:40:07 -04:00
commit ed99617c53

View file

@ -8,12 +8,15 @@ import {
type ServerSettingsSection,
} from "./ServerSettingsForm";
/** Find the "Clear" button living in the rightSection of `input`'s field. */
/** Find the clear button living in the rightSection of `input`'s field. The
* name is a prefix match because a KeyValueRows field names its clear button
* for the row it belongs to ("Clear header name, Cookie"), while a standalone
* field keeps the bare "Clear". */
function clearButtonFor(input: HTMLElement): HTMLElement {
const root =
input.closest('[class*="mantine-TextInput-root"]') ??
input.closest('[class*="Input-wrapper"]');
return within(root as HTMLElement).getByRole("button", { name: "Clear" });
return within(root as HTMLElement).getByRole("button", { name: /^Clear/ });
}
const emptySettings: InspectorServerSettings = {
@ -1391,12 +1394,12 @@ describe("ServerSettingsForm", () => {
expect(
within(
keyInput.closest('[class*="Input-wrapper"]') as HTMLElement,
).queryByRole("button", { name: "Clear" }),
).queryByRole("button", { name: /^Clear/ }),
).toBeNull();
expect(
within(
valueInput.closest('[class*="Input-wrapper"]') as HTMLElement,
).queryByRole("button", { name: "Clear" }),
).queryByRole("button", { name: /^Clear/ }),
).toBeNull();
});
@ -1419,12 +1422,12 @@ describe("ServerSettingsForm", () => {
expect(
within(
uriInput.closest('[class*="Input-wrapper"]') as HTMLElement,
).queryByRole("button", { name: "Clear" }),
).queryByRole("button", { name: /^Clear/ }),
).toBeNull();
expect(
within(
nameInput.closest('[class*="Input-wrapper"]') as HTMLElement,
).queryByRole("button", { name: "Clear" }),
).queryByRole("button", { name: /^Clear/ }),
).toBeNull();
// Typing into the URI threads the empty name through (`root.name ?? ""`).
await user.type(uriInput, "f");