// @vitest-environment jsdom // // A closed-set config key is picked, not typed: a short list renders as a segmented bar, a long one as // 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, except for an option's label: a full key would distort the // bar-or-dropdown choice, which weighs the real label length, so a choice id resolves to its value // under a one-character marker. The marker is what makes a lost lookup visible — rendering the bare // value instead of the id would be indistinguishable from `label: c`. 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"; import type { ConfigItem } from "../lib/api"; vi.mock("react-i18next", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, useTranslation: () => ({ // Only an id the fixture really defines resolves: a dropped lookup renders the bare value and // a malformed id renders the key (long enough to turn every bar into a dropdown), so the // assertions below see both. t: (key: string) => CHOICE_LABELS[key] ?? key, i18n: { language: "en" }, }), }; }); const choice = (key: string, choices: string[], value: string, is_set = false): ConfigItem => ({ key, type: "choice", group: "g", secret: false, min: null, max: null, is_set, allow_empty: false, choices, value, default: choices[0], }); const PRESETS = ["ultrafast", "superfast", "veryfast", "faster", "fast", "medium", "slow"]; const LEVELS = ["TRACE", "DEBUG", "INFO", "WARNING", "ERROR"]; const CHOICE_LABELS: Record = Object.fromEntries( ( [ ["plex_transcode_hw", ["auto", "on", "off"]], ["plex_transcode_preset", PRESETS], ["log_level", LEVELS], ] as const ).flatMap(([key, choices]) => choices.map((c) => [`config.choices.${key}.${c}`, `~${c}`] as const), ), ); const setConfig = vi.fn((_key: string, _value: unknown) => Promise.resolve({})); const resetConfig = vi.fn((_key: string) => Promise.resolve({})); vi.mock("../lib/api", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, api: { ...actual.api, adminConfig: () => Promise.resolve({ groups: { g: [ choice("plex_transcode_hw", ["auto", "on", "off"], "on", true), choice("plex_transcode_preset", PRESETS, "veryfast"), choice("log_level", LEVELS, "INFO"), ], }, secrets_manageable: true, }), setConfig: (key: string, value: unknown) => setConfig(key, value), resetConfig: (key: string) => resetConfig(key), }, }; }); vi.mock("../lib/useMe", () => ({ useMe: () => ({ email_configured: true }) })); import ConfigPanel from "./ConfigPanel"; afterEach(() => { cleanup(); vi.clearAllMocks(); }); const mount = () => render( , ); describe("ConfigPanel choice keys", () => { it("renders a short list as a segmented bar and saves the pick", async () => { mount(); 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(["~on"]); expect(screen.queryByRole("textbox")).toBeNull(); // no free-text field for a choice key 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")); }); it("shows the default as active once the override is reset, and Save resets it", async () => { 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: "~auto" }).getAttribute("aria-pressed")).toBe("true"); fireEvent.click(await screen.findByRole("button", { name: "config.save" })); await waitFor(() => expect(resetConfig).toHaveBeenCalledWith("plex_transcode_hw")); }); 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: ~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 === "~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(); }); });