fix(views): seed the default view only after the tab-account pins

The S2 seed effect ran on the saved-views query's first resolve, but that
query is ungated and — since SavedViewsProvider mounts above <App>, which pins
the tab-account only after /api/me — resolves before the account is known on a
fresh tab. It then evaluated canSeedDefault() against a null account (which
reports 'stored, do not seed') and burned its one-shot guard, so the starred
default was never applied until a reload: exactly the fresh-tab miss S2 set out
to fix. Gate the effect on the reactive account id so it defers instead of
consuming the guard, and re-fires when the account pins. Covers feed + plex.

Adds a dom test for the defer-then-seed-on-pin path the old test masked by
pre-pinning the account.
This commit is contained in:
2026-08-24 22:09:39 +02:00
parent 06b85586e6
commit 852b227ea2
2 changed files with 28 additions and 4 deletions
@@ -7,7 +7,7 @@
// Plex default at all. The sub-providers are mocked so we can assert exactly which setFilters is called;
// api.savedViews is stubbed and driven through a real QueryClient.
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, screen, waitFor, cleanup } from "@testing-library/react";
import { render, screen, waitFor, cleanup, act } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const h = vi.hoisted(() => ({
@@ -43,7 +43,7 @@ vi.mock("./PlexProvider", () => ({
vi.mock("./ConfirmProvider", () => ({ useConfirm: () => async () => true }));
import { SavedViewsProvider, useSavedViews } from "./SavedViewsProvider";
import { api, type SavedView } from "../lib/api";
import { api, setActiveAccount, type SavedView } from "../lib/api";
import { LS, accountKey } from "../lib/storage";
import { ACTIVE_ACCOUNT_KEY } from "../lib/activeAccount";
@@ -97,6 +97,21 @@ describe("SavedViewsProvider — default-view seeding on first resolve", () => {
expect(h.setFeedFilters.mock.calls[0]![0]).toMatchObject({ scope: "all", show: "watched" });
});
it("feed: on a fresh tab the query resolves before the account pins — defers, then seeds on the pin", async () => {
// The provider mounts above <App>, which pins the tab-account only after /api/me; the ungated
// views query resolves first. Seeding must WAIT for the account (not seed nothing and burn its
// one-shot — the pre-fix regression that left a fresh tab on DEFAULT_FILTERS until a reload).
sessionStorage.removeItem(ACTIVE_ACCOUNT_KEY);
await mountResolved([
view({ id: 2, is_default: true, filters: { scope: "all", show: "watched" } }),
]);
expect(h.setFeedFilters).not.toHaveBeenCalled();
// App pins the account; the reactive id re-fires the effect and the deferred seed lands.
act(() => setActiveAccount(ID));
await waitFor(() => expect(h.setFeedFilters).toHaveBeenCalledTimes(1));
expect(h.setFeedFilters.mock.calls[0]![0]).toMatchObject({ scope: "all", show: "watched" });
});
it("feed: does NOT seed when the account already has stored filters", async () => {
localStorage.setItem(accountKey(LS.filters, ID)!, JSON.stringify({ scope: "my" }));
await mountResolved([view({ id: 2, is_default: true, filters: { scope: "all" } })]);