diff --git a/frontend/src/components/ConfigPanel.choice.dom.test.tsx b/frontend/src/components/ConfigPanel.choice.dom.test.tsx index 8ad885fe..24a81332 100644 --- a/frontend/src/components/ConfigPanel.choice.dom.test.tsx +++ b/frontend/src/components/ConfigPanel.choice.dom.test.tsx @@ -4,7 +4,8 @@ // a dropdown, and the pick is what Save sends. A cleared draft (reset to the default) shows the default // as the active option rather than no option at all. // -// i18n is a key-passthrough; the admin config API is stubbed. +// i18n is a key-passthrough, except that an option's label is its value (the bar-or-dropdown choice +// weighs the real label length, which a full key would distort); the admin config API is stubbed. import { afterEach, describe, expect, it, vi } from "vitest"; import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; @@ -14,7 +15,10 @@ vi.mock("react-i18next", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, - useTranslation: () => ({ t: (key: string) => key, i18n: { language: "en" } }), + useTranslation: () => ({ + t: (key: string) => (key.startsWith("config.choices.") ? key.split(".").pop() : key), + i18n: { language: "en" }, + }), }; }); @@ -48,6 +52,7 @@ vi.mock("../lib/api", async (importOriginal) => { g: [ choice("plex_transcode_hw", ["auto", "on", "off"], "on", true), choice("plex_transcode_preset", PRESETS, "veryfast"), + choice("log_level", ["TRACE", "DEBUG", "INFO", "WARNING", "ERROR"], "INFO"), ], }, secrets_manageable: true, @@ -79,10 +84,10 @@ describe("ConfigPanel choice keys", () => { const bar = await screen.findByRole("group", { name: "config.fields.plex_transcode_hw.label" }); const pressed = () => [...bar.querySelectorAll('[aria-pressed="true"]')].map((b) => b.textContent); - expect(pressed()).toEqual(["config.choices.plex_transcode_hw.on"]); + expect(pressed()).toEqual(["on"]); expect(screen.queryByRole("textbox")).toBeNull(); // no free-text field for a choice key - fireEvent.click(screen.getByRole("button", { name: "config.choices.plex_transcode_hw.off" })); + fireEvent.click(screen.getByRole("button", { name: "off" })); fireEvent.click(await screen.findByRole("button", { name: "config.save" })); await waitFor(() => expect(setConfig).toHaveBeenCalledWith("plex_transcode_hw", "off")); }); @@ -91,11 +96,7 @@ describe("ConfigPanel choice keys", () => { mount(); await screen.findByRole("group", { name: "config.fields.plex_transcode_hw.label" }); fireEvent.click(screen.getByRole("button", { name: /config\.reset$/ })); - expect( - screen - .getByRole("button", { name: "config.choices.plex_transcode_hw.auto" }) - .getAttribute("aria-pressed"), - ).toBe("true"); + expect(screen.getByRole("button", { name: "auto" }).getAttribute("aria-pressed")).toBe("true"); fireEvent.click(await screen.findByRole("button", { name: "config.save" })); await waitFor(() => expect(resetConfig).toHaveBeenCalledWith("plex_transcode_hw")); }); @@ -103,16 +104,20 @@ describe("ConfigPanel choice keys", () => { it("renders a long list as a dropdown showing only the value", async () => { mount(); const trigger = await screen.findByRole("button", { - name: "config.fields.plex_transcode_preset.label: config.choices.plex_transcode_preset.veryfast", + name: "config.fields.plex_transcode_preset.label: veryfast", }); - expect(trigger.textContent).toBe("config.choices.plex_transcode_preset.veryfast"); + expect(trigger.textContent).toBe("veryfast"); fireEvent.click(trigger); // The popover rows are queried as FilterSelect.dom.test does (`hidden: true`, picked by text). const rows = screen.getAllByRole("menuitemradio", { hidden: true }); - fireEvent.click( - rows.find((r) => r.textContent === "config.choices.plex_transcode_preset.slow")!, - ); + fireEvent.click(rows.find((r) => r.textContent === "slow")!); fireEvent.click(await screen.findByRole("button", { name: "config.save" })); await waitFor(() => expect(setConfig).toHaveBeenCalledWith("plex_transcode_preset", "slow")); }); + + it("a few options with long labels are a dropdown too: a bar that wide does not fit a phone", async () => { + mount(); + await screen.findByRole("button", { name: "config.fields.log_level.label: INFO" }); + expect(screen.queryByRole("group", { name: "config.fields.log_level.label" })).toBeNull(); + }); }); diff --git a/frontend/src/components/ConfigPanel.tsx b/frontend/src/components/ConfigPanel.tsx index 5e0e4522..431dce7c 100644 --- a/frontend/src/components/ConfigPanel.tsx +++ b/frontend/src/components/ConfigPanel.tsx @@ -340,7 +340,9 @@ function Field({ const showReset = item.is_set && !isBool; return ( -
+ // Stacked below `sm`: the control column cannot shrink, and beside a wide control it left the + // label and hint a sliver of a phone's width. +
{label}

@@ -349,7 +351,7 @@ function Field({

{status}

-
+
{isBool ? ( sum + o.label.length, 0); + if (options.length <= MAX_SEGMENTS && labelChars <= MAX_SEGMENT_CHARS) { return (