diff --git a/frontend/src/components/SavedViewsProvider.dom.test.tsx b/frontend/src/components/SavedViewsProvider.dom.test.tsx index ea9d1459..da28f489 100644 --- a/frontend/src/components/SavedViewsProvider.dom.test.tsx +++ b/frontend/src/components/SavedViewsProvider.dom.test.tsx @@ -67,12 +67,19 @@ function Probe() { ); } +// The views query waits for a signed-in account in the cache (BootGate's `me`). +function signedInClient() { + const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } }); + qc.setQueryData(qk.me(), { id: ID, preferences: {} }); + return qc; +} + async function mountResolved(views: SavedView[]) { // Fresh array + object copies per call, like a real fetch — so a refetch changes viewsQuery.data's // identity and re-runs the seed effect (which is what makes the per-source guard actually load-bearing // in the re-seed test; a shared reference would skip the effect and hide a missing guard). vi.spyOn(api, "savedViews").mockImplementation(async () => views.map((v) => ({ ...v }))); - const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } }); + const qc = signedInClient(); render( @@ -141,7 +148,7 @@ describe("SavedViewsProvider — default-view seeding on first resolve", () => { calls += 1; return calls === 1 ? [def] : [def, view({ id: 9, name: "added-later" })]; }); - const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } }); + const qc = signedInClient(); render( @@ -174,3 +181,26 @@ describe("SavedViewsProvider — default-view seeding on first resolve", () => { expect(h.setPlexFilters).not.toHaveBeenCalled(); }); }); + +describe("SavedViewsProvider — signed-out tab", () => { + it("does NOT ask for saved views while no account is signed in (the landing logged a 401)", async () => { + const spy = vi.spyOn(api, "savedViews").mockResolvedValue([]); + const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } }); + qc.setQueryData(qk.me(), null); // BootGate's "nobody" + render( + + + + + , + ); + // Let any fetch the mount would start get going before asserting it never happened. + await act(async () => { + await new Promise((r) => setTimeout(r, 20)); + }); + expect(spy).not.toHaveBeenCalled(); + // …and it asks as soon as an account signs in. + act(() => qc.setQueryData(qk.me(), { id: ID, preferences: {} })); + await waitFor(() => expect(spy).toHaveBeenCalledWith("feed")); + }); +}); diff --git a/frontend/src/components/SavedViewsProvider.tsx b/frontend/src/components/SavedViewsProvider.tsx index 47ae71db..6b2ab11f 100644 --- a/frontend/src/components/SavedViewsProvider.tsx +++ b/frontend/src/components/SavedViewsProvider.tsx @@ -155,9 +155,14 @@ export function SavedViewsProvider({ children }: { children: ReactNode }) { }, [page, feedFilters, plexFilters, setFeedFilters, setPlexFilters]); const source = cfg.source; + // Passive observer of the account BootGate fetches: the providers mount for a signed-out tab too + // (the public landing), and saved views are per-account, so asking for "nobody's" views only logs + // a 401 on the landing page. Enabled once a signed-in account is cached. + const { data: me } = useQuery({ queryKey: qk.me(), queryFn: api.me, enabled: false }); const viewsQuery = useQuery({ queryKey: qk.savedViews(source), queryFn: () => api.savedViews(source), + enabled: !!me, }); const views = useMemo(() => viewsQuery.data ?? [], [viewsQuery.data]);