Skip to content

Commit 1c36e7a

Browse files
committed
fix(gui): stop the account-pool card from printing its title twice
The Codex account-pool card rendered "Rotation strategy" twice in a row: once as the card title in CodexPoolStrategySetting, and again as the select's field label inside the shared AccountPoolStrategyControls. Both read the same i18n key, so every locale showed the duplicate. Give the shared control a `strategyLabelHidden` prop and set it on the Codex card, where the title already names the field. The label element stays in the tree as `.sr-only` and the select keeps its aria-label, so the accessible name is unchanged — only the duplicated on-screen text is gone. The Anthropic pool card titles itself `anthropicPool.title`, so its visual label is not a duplicate; the prop defaults to false and leaves that surface untouched. No i18n keys were added, renamed, or removed, so all locales stay in sync, and `.sr-only` already exists in styles.css.
1 parent c7e48fb commit 1c36e7a

3 files changed

Lines changed: 85 additions & 1 deletion

File tree

gui/src/components/AccountPoolStrategyControls.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ export interface AccountPoolStrategyControlsProps {
1616
disabled?: boolean;
1717
strategySelectId?: string;
1818
stickyInputId?: string;
19+
/**
20+
* Hide the visual strategy label when the surrounding card title already reads
21+
* "Rotation strategy". The select keeps its aria-label, so the accessible name
22+
* survives while the duplicated on-screen text disappears.
23+
*/
24+
strategyLabelHidden?: boolean;
1925
onStrategyChange(strategy: AccountPoolStrategy): void;
2026
onStickyDraftChange(value: string): void;
2127
onStickyCommit(): void;
@@ -30,6 +36,7 @@ export default function AccountPoolStrategyControls({
3036
disabled = false,
3137
strategySelectId = "account-pool-strategy",
3238
stickyInputId = "account-pool-sticky-limit",
39+
strategyLabelHidden = false,
3340
onStrategyChange,
3441
onStickyDraftChange,
3542
onStickyCommit,
@@ -38,7 +45,9 @@ export default function AccountPoolStrategyControls({
3845
return (
3946
<div style={{ marginTop: 12 }}>
4047
<label className="field" style={{ display: "block" }} htmlFor={strategySelectId}>
41-
<span className="field-label">{t("accountPool.strategy")}</span>
48+
<span className={strategyLabelHidden ? "sr-only" : "field-label"}>
49+
{t("accountPool.strategy")}
50+
</span>
4251
<select
4352
id={strategySelectId}
4453
className="input"

gui/src/components/CodexPoolStrategySetting.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ export default function CodexPoolStrategySetting({ apiBase }: { apiBase: string
122122
disabled={saving}
123123
strategySelectId="codex-pool-strategy"
124124
stickyInputId="codex-pool-sticky-limit"
125+
strategyLabelHidden
125126
onStrategyChange={(next) => {
126127
if (next === strategy) return;
127128
void save({ strategy: next });

gui/tests/account-pool-strategy.test.tsx

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,41 @@ describe("AccountPoolStrategyControls", () => {
175175
expect(rr).toContain("Sticky successes before rotate");
176176
expect(rr).toContain('value="2"');
177177
});
178+
179+
test("keeps the visual field label by default (Anthropic card has its own title)", () => {
180+
const markup = renderToStaticMarkup(
181+
<LanguageProvider>
182+
<AccountPoolStrategyControls
183+
strategy="quota"
184+
stickyDraft="1"
185+
onStrategyChange={() => {}}
186+
onStickyDraftChange={() => {}}
187+
onStickyCommit={() => {}}
188+
/>
189+
</LanguageProvider>,
190+
);
191+
expect(markup).toContain('class="field-label"');
192+
expect(markup).not.toContain('class="sr-only"');
193+
});
194+
195+
test("strategyLabelHidden drops the duplicate visual label but keeps the accessible name", () => {
196+
const markup = renderToStaticMarkup(
197+
<LanguageProvider>
198+
<AccountPoolStrategyControls
199+
strategy="quota"
200+
stickyDraft="1"
201+
strategyLabelHidden
202+
onStrategyChange={() => {}}
203+
onStickyDraftChange={() => {}}
204+
onStickyCommit={() => {}}
205+
/>
206+
</LanguageProvider>,
207+
);
208+
expect(markup).not.toContain('class="field-label"');
209+
expect(markup).toContain('class="sr-only"');
210+
// The select must still expose an accessible name.
211+
expect(markup).toContain('aria-label="Rotation strategy"');
212+
});
178213
});
179214

180215
describe("CodexPoolStrategySetting optimistic strategy select", () => {
@@ -282,4 +317,43 @@ describe("CodexPoolStrategySetting optimistic strategy select", () => {
282317
expect(select()?.value).toBe("quota");
283318
expect(host.textContent).toContain("Rotation strategy could not be saved");
284319
});
320+
321+
test("renders the card title once, without a duplicate field label", async () => {
322+
globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
323+
const url = String(input);
324+
if (url.endsWith("/api/codex-auth/active") && (!init || init.method === undefined)) {
325+
return new Response(JSON.stringify({
326+
accountPoolStrategy: "quota",
327+
accountPoolStickyLimit: 1,
328+
}), { status: 200 });
329+
}
330+
throw new Error(`unexpected fetch: ${url} ${init?.method ?? "GET"}`);
331+
}) as typeof fetch;
332+
333+
const host = testWindow.document.createElement("div");
334+
testWindow.document.body.appendChild(host as never);
335+
const { createRoot } = await import("react-dom/client");
336+
await act(async () => {
337+
mountedRoot = createRoot(host);
338+
mountedRoot.render(
339+
<LanguageProvider>
340+
<CodexPoolStrategySetting apiBase="http://proxy" />
341+
</LanguageProvider>,
342+
);
343+
});
344+
await act(async () => { await flush(); });
345+
346+
// The card title is the only VISIBLE occurrence; the select's label is sr-only.
347+
const visible = [...host.querySelectorAll("*")]
348+
.filter((el) => !el.classList.contains("sr-only"))
349+
.filter((el) => el.children.length === 0)
350+
.map((el) => el.textContent?.trim() ?? "")
351+
.filter((text) => text === "Rotation strategy");
352+
expect(visible).toHaveLength(1);
353+
expect(host.querySelector(".field-label")).toBeNull();
354+
355+
// Accessibility is preserved: the select still has an accessible name.
356+
const select = host.querySelector<HTMLSelectElement>("#codex-pool-strategy");
357+
expect(select?.getAttribute("aria-label")).toBe("Rotation strategy");
358+
});
285359
});

0 commit comments

Comments
 (0)