The code cited its plan instead of saying why: `E28 review J4`, `(R6 S3b)`, `S1 justified…`,
`deep-review round 5`. That plan lives in the private ops repo, so for every reader of this one the
citation was noise, and a sentence whose subject was the plan step ("round 2 showed…") hid its
lesson behind an id. Each one is gone and the "why" kept: a bare citation is deleted, a sentence
carrying a lesson is reworded around it ("a later attempt showed…").
Measured (`scripts/audit_metrics.py`, product code): 652 lines in 270 files carried an epic code by
the definition the next commit holds (507 / 241 by the old, two-digit-only one) -> 0. Across all
code, tests, scripts and configs: ~1100 epic-code lines plus ~320 sprint/round ids.
Behaviour unchanged, checked mechanically: every changed .py file has the same AST once docstrings
are blanked; css/sh/toml/yml/json files are identical once comments are stripped; the only
non-comment changes in TS/JS are test titles, one description string in a contract table, and five
ESLint messages, whose text is all the gates snapshot diff holds. Not touched: Markdown (docs and
the changelog keep their history), Alembic migrations, TV episode notation (`S2 · E5`), DESIGN.md
ids (`C1`, `B4`, `S3h`), tracker ids (`SEC-16`, `DX-17`).
One stale claim fixed on the way: test_hls_reaper's docstring said the player checkpoints "every
10 s whether or not it is playing", which plex/stream.py calls wrong; it now says what the code does.
210 lines
8.7 KiB
TypeScript
210 lines
8.7 KiB
TypeScript
// @vitest-environment jsdom
|
|
//
|
|
// The single default-application path. On the saved-views query's FIRST resolve, SavedViewsProvider
|
|
// seeds the starred default view's filters into a genuinely fresh module (no stored filters), for BOTH
|
|
// the feed and Plex — but leaves them alone once the account has its own stored filters. This is the
|
|
// fix for the old feed-only LS mirror that raced FeedFiltersProvider's login effect and never applied a
|
|
// 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, act } from "@testing-library/react";
|
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
|
|
const h = vi.hoisted(() => ({
|
|
page: "feed" as "feed" | "plex",
|
|
setFeedFilters: vi.fn(),
|
|
setPlexFilters: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("react-i18next", async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import("react-i18next")>();
|
|
return { ...actual, useTranslation: () => ({ t: (key: string) => key }) };
|
|
});
|
|
vi.mock("./navigationContext", () => ({ useNavigation: () => ({ page: h.page }) }));
|
|
vi.mock("./feedFiltersContext", () => ({
|
|
useFeedFilters: () => ({
|
|
tags: [],
|
|
channelIds: [],
|
|
tagModes: { language: "or", topic: "or", other: "or" },
|
|
q: "",
|
|
sort: "newest",
|
|
scope: "my",
|
|
includeNormal: true,
|
|
includeShorts: false,
|
|
includeLive: false,
|
|
show: "unwatched",
|
|
}),
|
|
useFeedFiltersActions: () => ({ setFilters: h.setFeedFilters }),
|
|
}));
|
|
vi.mock("./plexContext", () => ({
|
|
usePlex: () => ({ filters: {} }),
|
|
usePlexActions: () => ({ setFilters: h.setPlexFilters }),
|
|
}));
|
|
vi.mock("./confirmContext", () => ({ useConfirm: () => async () => true }));
|
|
|
|
import { SavedViewsProvider } from "./SavedViewsProvider";
|
|
import { useSavedViews } from "./savedViewsContext";
|
|
import { api, type SavedView } from "../lib/api";
|
|
import { LS, accountKey } from "../lib/storage";
|
|
import { ACTIVE_ACCOUNT_KEY } from "../lib/activeAccount";
|
|
import { qk } from "../lib/queryKeys";
|
|
|
|
const ID = 7;
|
|
const view = (over: Partial<SavedView>): SavedView => ({
|
|
id: 1,
|
|
name: "v",
|
|
filters: {},
|
|
position: 0,
|
|
is_default: false,
|
|
...over,
|
|
});
|
|
|
|
function Probe() {
|
|
const { loading, views } = useSavedViews();
|
|
return (
|
|
<div data-testid="loading" data-count={views.length}>
|
|
{String(loading)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// 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 = signedInClient();
|
|
render(
|
|
<QueryClientProvider client={qc}>
|
|
<SavedViewsProvider>
|
|
<Probe />
|
|
</SavedViewsProvider>
|
|
</QueryClientProvider>,
|
|
);
|
|
// Wait for the query to resolve so the first-resolve effect has run.
|
|
await waitFor(() => expect(screen.getByTestId("loading").textContent).toBe("false"));
|
|
return qc;
|
|
}
|
|
|
|
beforeEach(() => {
|
|
localStorage.clear();
|
|
sessionStorage.setItem(ACTIVE_ACCOUNT_KEY, String(ID));
|
|
// Reset the URL: feedCanSeedDefault reads window.location.search, and a stale ?sort= from a prior
|
|
// case would suppress seeding in the others.
|
|
window.history.replaceState({}, "", "/");
|
|
h.page = "feed";
|
|
h.setFeedFilters.mockClear();
|
|
h.setPlexFilters.mockClear();
|
|
});
|
|
afterEach(() => {
|
|
cleanup();
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
describe("SavedViewsProvider — default-view seeding on first resolve", () => {
|
|
it("feed: seeds the starred default into a fresh module (no stored filters)", async () => {
|
|
await mountResolved([
|
|
view({ id: 2, is_default: true, filters: { scope: "all", show: "watched" } }),
|
|
]);
|
|
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" } })]);
|
|
expect(h.setFeedFilters).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("feed: does NOT seed when no view is starred as default", async () => {
|
|
await mountResolved([view({ id: 2, is_default: false, filters: { scope: "all" } })]);
|
|
expect(h.setFeedFilters).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("feed: does NOT seed when the URL carries filter params (a shared/bookmarked view wins)", async () => {
|
|
// A link like ?sort=oldest is an explicit filter the user arrived on; the starred default must not
|
|
// stomp it. feedCanSeedDefault() bails on hasFilterParams — plex ignores the URL, so this is feed-only.
|
|
window.history.replaceState({}, "", "/?sort=oldest");
|
|
await mountResolved([
|
|
view({ id: 2, is_default: true, filters: { scope: "all", show: "watched" } }),
|
|
]);
|
|
expect(h.setFeedFilters).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("does NOT re-seed on a background refetch that returns a changed list (per-source one-shot guard)", async () => {
|
|
// The 60s refetch re-resolves the query; when it returns a CHANGED list (e.g. the user just added a
|
|
// view elsewhere) react-query hands down a new reference and the seed effect re-runs. seededRef must
|
|
// stop it re-applying the default over filters the user has since edited. (An identical refetch is
|
|
// deduped by structural sharing and never reaches the effect, so the changed list is the real test.)
|
|
const def = view({ id: 2, is_default: true, filters: { scope: "all", show: "watched" } });
|
|
let calls = 0;
|
|
vi.spyOn(api, "savedViews").mockImplementation(async () => {
|
|
calls += 1;
|
|
return calls === 1 ? [def] : [def, view({ id: 9, name: "added-later" })];
|
|
});
|
|
const qc = signedInClient();
|
|
render(
|
|
<QueryClientProvider client={qc}>
|
|
<SavedViewsProvider>
|
|
<Probe />
|
|
</SavedViewsProvider>
|
|
</QueryClientProvider>,
|
|
);
|
|
await waitFor(() => expect(screen.getByTestId("loading").textContent).toBe("false"));
|
|
expect(h.setFeedFilters).toHaveBeenCalledTimes(1);
|
|
await act(async () => {
|
|
await qc.refetchQueries({ queryKey: qk.savedViews("feed") });
|
|
});
|
|
// Wait until the changed list has actually reached the component, so the seed effect has had its
|
|
// chance to (wrongly) re-fire — otherwise the assertion could win by racing ahead of the re-render.
|
|
await waitFor(() => expect(screen.getByTestId("loading").dataset.count).toBe("2"));
|
|
expect(h.setFeedFilters).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("plex: seeds the starred default into a fresh module (parity with the feed)", async () => {
|
|
h.page = "plex";
|
|
await mountResolved([view({ id: 3, is_default: true, filters: { genres: ["Action"] } })]);
|
|
expect(h.setPlexFilters).toHaveBeenCalledTimes(1);
|
|
expect(h.setPlexFilters.mock.calls[0]![0]).toMatchObject({ genres: ["Action"] });
|
|
});
|
|
|
|
it("plex: does NOT seed when plex filters are already stored", async () => {
|
|
h.page = "plex";
|
|
localStorage.setItem(accountKey(LS.plexFilters, ID)!, JSON.stringify({ genres: ["Comedy"] }));
|
|
await mountResolved([view({ id: 3, is_default: true, filters: { genres: ["Action"] } })]);
|
|
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(
|
|
<QueryClientProvider client={qc}>
|
|
<SavedViewsProvider>
|
|
<Probe />
|
|
</SavedViewsProvider>
|
|
</QueryClientProvider>,
|
|
);
|
|
// 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"));
|
|
});
|
|
});
|