fix(e24): review round 3 — the holes round 2's own fixes left
Round 3 reviewed the round-2 commit across six dimensions. Fifteen findings, five survived three adversarial refuters each; three died on 2+ refutations (a keyboard-focus claim that is a property of `ui/Button` at all six of its `loading` call sites, a contract-test false-pass that needs a deliberate revert plus a self-incriminating comment, and a MAX_EXCLUDE coupling whose slice is unreachable from the real client). - `rerolling` was still wrong for a FAILED press. `data === undefined` is true forever for a key whose request failed (`retry: false`), so the next unrelated mutation re-disabled the reroll button — the same defect round 2 reported, one branch over. `!isFetched` is the flag that remembers a key settled; `!isError` would not have worked, because query-core resets a data-less query to "pending" for the duration of a refetch, hiding the error exactly when it matters. - The hero fallback used `??`, which cannot tell "this budget's last pick resolved to NO hero" from "nothing remembered". So a press that failed after an empty one resurrected the card the page had already excluded as seen and just said it had nothing beyond. The tagged slot is now authoritative for its own budget. - `setPage`'s boolean — the input the whole burial rule reads — was asserted by nothing. The type fences the SHAPE, so deleting a `return` fails tsc, but swapping the two values passed typecheck and all 822 tests. NavigationProvider gets its first render test, on the value AND on the effects the early return skips. - `closeThenNavigate`'s door into `buryUnder` was equally unpinned: reverting that one call site to the old unconditional burial left the suite green. It has the mirror case now. - The comment claiming one rule serves both entry points was overstated. `buryUnder` fails OPEN, and `CommandItem.run` is typed `() => void`, so only the palette's bare `() => setPage(page)` rows forward the boolean at all. Nothing regressed — that path buried unconditionally before — but the prose said more than the code does, so it now says what is true and points at the deferred fix. Same for a "the only assertion in the suite" claim that the file it cites as precedent also makes. Every new test mutation-checked red for the right reason, both edges where there are two. Gate: ruff, ruff format, 3x tsc, eslint (0 errors), prettier, knip, vitest 828, pytest 1005.
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
// @vitest-environment jsdom
|
||||
//
|
||||
// `setPage`'s no-op contract — the PRODUCER side of the history-burial fix, which nothing rendered
|
||||
// before this. `setPage` early-returns when the requested page is already current with no channel
|
||||
// page over it, and reports that by returning `false`; `lib/history.buryUnder` reads the boolean to
|
||||
// decide whether the overlay entries beneath a navigation are still reclaimable. Get it wrong and
|
||||
// the round-2 bug is back: Shift+W over the player on Home buries an entry nothing pushed past, and
|
||||
// the user's next browser Back does nothing.
|
||||
//
|
||||
// The type does NOT fence this. `NavigationActions` pins the SHAPE (`=> boolean`), so deleting one
|
||||
// `return` fails tsc — but round 3 measured that swapping the two values (early return `true`, tail
|
||||
// `false`) passes typecheck and all 822 tests. Hence assertions on the value AND on the effects the
|
||||
// early return skips, which is what the boolean actually stands for.
|
||||
import { describe, it, expect, beforeEach, vi } from "vitest";
|
||||
import { renderHook, act } from "@testing-library/react";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
// Partial: the provider only wants `t` for the document title, but the i18n module this pulls in
|
||||
// initialises against the real `initReactI18next`.
|
||||
vi.mock("react-i18next", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("react-i18next")>();
|
||||
return {
|
||||
...actual,
|
||||
useTranslation: () => ({ t: (k: string) => k, i18n: { language: "en" } }),
|
||||
};
|
||||
});
|
||||
|
||||
import { NavigationProvider, useNavigation, useNavigationActions } from "./NavigationProvider";
|
||||
|
||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<NavigationProvider>{children}</NavigationProvider>
|
||||
);
|
||||
const render = () =>
|
||||
renderHook(() => ({ ...useNavigation(), ...useNavigationActions() }), { wrapper });
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
sessionStorage.setItem("siftlode.activeAccount", "1"); // account-scoped page memory
|
||||
window.history.replaceState({ sfPage: "feed" }, "", "/");
|
||||
});
|
||||
|
||||
describe("NavigationProvider · setPage reports whether it navigated", () => {
|
||||
it("returns true and pushes an entry for a real page change", () => {
|
||||
const { result } = render();
|
||||
const push = vi.spyOn(window.history, "pushState");
|
||||
try {
|
||||
let reported: boolean | undefined;
|
||||
act(() => {
|
||||
reported = result.current.setPage("plex");
|
||||
});
|
||||
expect(reported).toBe(true);
|
||||
expect(push).toHaveBeenCalledTimes(1);
|
||||
expect(result.current.page).toBe("plex");
|
||||
} finally {
|
||||
push.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("returns false and pushes nothing when it is already that page", () => {
|
||||
const { result } = render();
|
||||
act(() => {
|
||||
result.current.setPage("plex");
|
||||
});
|
||||
const push = vi.spyOn(window.history, "pushState");
|
||||
try {
|
||||
let reported: boolean | undefined;
|
||||
act(() => {
|
||||
reported = result.current.setPage("plex");
|
||||
});
|
||||
// Both halves matter: `false` is what `buryUnder` reads, and "pushed nothing" is what makes
|
||||
// that answer true. A value-only assertion would pass against an inverted pair.
|
||||
expect(reported).toBe(false);
|
||||
expect(push).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
push.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("returns true even for the current page when a channel page is over it", () => {
|
||||
// The nav rail's "Feed" while a channel opened from the feed is a real navigation: it closes the
|
||||
// channel page. The `&& !channelViewRef.current` half of the guard is what makes it one.
|
||||
const { result } = render();
|
||||
act(() => {
|
||||
result.current.openChannel("UC1", "A channel");
|
||||
});
|
||||
expect(result.current.channelView).not.toBeNull();
|
||||
|
||||
const push = vi.spyOn(window.history, "pushState");
|
||||
try {
|
||||
let reported: boolean | undefined;
|
||||
act(() => {
|
||||
reported = result.current.setPage("feed");
|
||||
});
|
||||
expect(reported).toBe(true);
|
||||
expect(push).toHaveBeenCalledTimes(1);
|
||||
expect(result.current.channelView).toBeNull();
|
||||
} finally {
|
||||
push.mockRestore();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user