diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx
index bfa6fbca..3d2498a1 100644
--- a/frontend/src/components/Header.tsx
+++ b/frontend/src/components/Header.tsx
@@ -94,7 +94,7 @@ export default function Header({
// Absolutely positioned within the (relative) content column, so its left edge tracks the
// actual left zone: it sits just inside the content, reclaiming the space when the nav rail is
// slim and/or no filter sidebar is present — no hard-coded offset.
-
+
{
+ const actual = await importOriginal();
+ return { ...actual, useTranslation: () => ({ t: (key: string) => key }) };
+});
+
+const plexState = { scope: "both" } as { scope: string };
+const setScope = vi.fn();
+vi.mock("./PlexProvider", () => ({
+ usePlex: () => plexState,
+ usePlexActions: () => ({ setScope }),
+}));
+
+import PlexScopeToggle from "./PlexScopeToggle";
+
+beforeEach(() => {
+ plexState.scope = "both";
+ setScope.mockReset();
+});
+afterEach(cleanup);
+
+describe("PlexScopeToggle", () => {
+ it("renders the three library scopes, current one pressed", () => {
+ plexState.scope = "movie";
+ render();
+ const group = screen.getByRole("group", { name: "plex.filter.scope" });
+ const btns = [...group.querySelectorAll("button")];
+ expect(btns.map((b) => b.textContent)).toEqual([
+ "plex.filter.scopeOpt.both",
+ "plex.filter.scopeOpt.movie",
+ "plex.filter.scopeOpt.show",
+ ]);
+ const pressed = btns.filter((b) => b.getAttribute("aria-pressed") === "true");
+ expect(pressed.map((b) => b.textContent)).toEqual(["plex.filter.scopeOpt.movie"]);
+ });
+
+ it("writes the picked scope back to the provider", () => {
+ render();
+ fireEvent.click(screen.getByRole("button", { name: "plex.filter.scopeOpt.show" }));
+ expect(setScope).toHaveBeenCalledWith("show");
+ });
+});
diff --git a/frontend/src/components/PlexSearchBox.tsx b/frontend/src/components/PlexSearchBox.tsx
index 69311b42..c6fc3e8f 100644
--- a/frontend/src/components/PlexSearchBox.tsx
+++ b/frontend/src/components/PlexSearchBox.tsx
@@ -117,10 +117,10 @@ export default function PlexSearchBox() {
leading={
- {pillLabel}
+ {pillLabel}
>
}
diff --git a/frontend/src/components/usePlexActiveFilters.dom.test.tsx b/frontend/src/components/usePlexActiveFilters.dom.test.tsx
new file mode 100644
index 00000000..d141f4bd
--- /dev/null
+++ b/frontend/src/components/usePlexActiveFilters.dom.test.tsx
@@ -0,0 +1,43 @@
+// @vitest-environment jsdom
+//
+// Guards the S3 behavior change: the active-filter chip row (usePlexActiveFilters) still emits chips
+// for the in-panel narrowings (expanded filters + watch-state) but NO LONGER emits a sort chip —
+// sort is an always-visible control above the grid now, like the Feed. i18n is key-passthrough and
+// the provider is mocked so the hook can be driven with a non-default sort present.
+import { describe, it, expect, vi } from "vitest";
+import { renderHook } from "@testing-library/react";
+import { EMPTY_PLEX_FILTERS } from "../lib/api";
+
+vi.mock("react-i18next", async (importOriginal) => {
+ const actual = await importOriginal();
+ return { ...actual, useTranslation: () => ({ t: (key: string) => key }) };
+});
+
+const plexState = {
+ scope: "both",
+ show: "watched",
+ // A non-default sort is present in the shared state — the hook must IGNORE it (no sort chip).
+ sort: { key: "year", dir: "asc" },
+ filters: { ...EMPTY_PLEX_FILTERS, genres: ["Action"] },
+};
+const setShow = vi.fn();
+const setFilters = vi.fn();
+vi.mock("./PlexProvider", () => ({
+ usePlex: () => plexState,
+ usePlexActions: () => ({ setShow, setFilters }),
+}));
+
+import { usePlexActiveFilters } from "./usePlexActiveFilters";
+
+describe("usePlexActiveFilters", () => {
+ it("emits filter + watch-state chips but never a sort chip", () => {
+ const { result } = renderHook(() => usePlexActiveFilters());
+ const keys = result.current.map((c) => c.key);
+ // The in-panel narrowings still surface...
+ expect(keys).toContain("genre:Action");
+ expect(keys).toContain("show");
+ // ...but sort does not, even though a non-default sort is active in the shared state.
+ expect(keys).not.toContain("sort");
+ expect(result.current.every((c) => !c.key.startsWith("sort"))).toBe(true);
+ });
+});