fix(history): a step back never leaves the app for an older page
Chrome keeps 50 entries a tab, and when the list is full a new entry drops the oldest one the page left without a user gesture, which can be the page's own entry under a sub-view or an overlay. A programmatic back() then left the document; once it landed on an older entry whose restored Plex player started playing on its own. Every programmatic back() now asks the Navigation API first and, with no entry of this document below, settles in place: the sub-view turns to its root, an overlay's entry drops its mark, the channel page and the YouTube search clear.
This commit is contained in:
@@ -32,6 +32,20 @@ import { PageHeading } from "./ui/Headings";
|
||||
|
||||
import { qk } from "../lib/queryKeys";
|
||||
|
||||
/** The Navigation API's view of the tab: `below` = whether each entry under the current one belongs
|
||||
* to this document, nearest first. jsdom has no Navigation API; Chrome's says this. */
|
||||
function stubNavigation(below: boolean[]) {
|
||||
const entries = [...below].reverse().map((sameDocument) => ({ sameDocument }));
|
||||
entries.push({ sameDocument: true });
|
||||
Object.defineProperty(window, "navigation", {
|
||||
configurable: true,
|
||||
value: { currentEntry: { index: entries.length - 1 }, entries: () => entries },
|
||||
});
|
||||
}
|
||||
afterEach(() => {
|
||||
Reflect.deleteProperty(window, "navigation");
|
||||
});
|
||||
|
||||
// The provider reads the signed-in account (the tab title) from the `me` cache.
|
||||
let qc = new QueryClient();
|
||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||
@@ -206,6 +220,31 @@ describe("NavigationProvider · Back", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("NavigationProvider · leaving a sub-view whose entry is the oldest of this document", () => {
|
||||
// A full tab can have dropped the entry under the one the channel page or the search pushed (see
|
||||
// lib/history's backStaysInDocument): leaving clears it in place instead of stepping out.
|
||||
it("closes the channel page in place", () => {
|
||||
const { result } = render();
|
||||
act(() => result.current.openChannel("UC1", "One"));
|
||||
stubNavigation([false]);
|
||||
const back = vi.spyOn(window.history, "back").mockImplementation(() => {});
|
||||
act(() => result.current.closeChannel());
|
||||
expect(back).not.toHaveBeenCalled();
|
||||
expect(result.current.channelView).toBeNull();
|
||||
expect(window.location.search).not.toContain("chan=");
|
||||
});
|
||||
|
||||
it("leaves the YouTube search in place", () => {
|
||||
const { result } = render();
|
||||
act(() => result.current.enterYtSearch("cats"));
|
||||
stubNavigation([false]);
|
||||
const back = vi.spyOn(window.history, "back").mockImplementation(() => {});
|
||||
act(() => result.current.exitYtSearch());
|
||||
expect(back).not.toHaveBeenCalled();
|
||||
expect(result.current.ytSearch).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("NavigationProvider · never under an open overlay", () => {
|
||||
it("setPage closes the overlay first and pushes only once its entry is reclaimed", async () => {
|
||||
const { result } = render();
|
||||
|
||||
Reference in New Issue
Block a user