108 lines
3.0 KiB
TypeScript
108 lines
3.0 KiB
TypeScript
import { afterEach, describe, expect, test, vi } from "vitest";
|
|
import {
|
|
act,
|
|
fireEvent,
|
|
render,
|
|
renderHook,
|
|
screen,
|
|
} from "@testing-library/react";
|
|
import { type ReactNode } from "react";
|
|
import { MemoryRouter, Route, Routes, useNavigate } from "react-router";
|
|
import { useFeedCache } from "../../../hooks/feed/useFeedCache.ts";
|
|
|
|
afterEach(() => vi.useRealTimers());
|
|
|
|
describe("useFeedCache", () => {
|
|
// A MemoryRouter's first entry is a POP, as after Back.
|
|
const wrapper = () => ({ children }: { children: ReactNode }) => (
|
|
<MemoryRouter>{children}</MemoryRouter>
|
|
);
|
|
|
|
test("restores a saved feed on a back/forward (POP) visit", () => {
|
|
const first = renderHook(() => useFeedCache<{ n: number }>("k", (x) => x), {
|
|
wrapper: wrapper(),
|
|
});
|
|
act(() =>
|
|
first.result.current.saveState({
|
|
items: [{ n: 1 }],
|
|
page: 2,
|
|
hasMore: true,
|
|
}, 300)
|
|
);
|
|
const second = renderHook(
|
|
() => useFeedCache<{ n: number }>("k", (x) => ({ n: x.n * 10 })),
|
|
{ wrapper: wrapper() },
|
|
);
|
|
expect(second.result.current.cached).toMatchObject({
|
|
items: [{ n: 10 }],
|
|
page: 2,
|
|
hasMore: true,
|
|
scrollY: 300,
|
|
});
|
|
});
|
|
|
|
test("an expired entry is ignored", () => {
|
|
vi.useFakeTimers();
|
|
const first = renderHook(() => useFeedCache("k", (x) => x), {
|
|
wrapper: wrapper(),
|
|
});
|
|
act(() =>
|
|
first.result.current.saveState({ items: [], page: 1, hasMore: false }, 0)
|
|
);
|
|
vi.advanceTimersByTime(11 * 60 * 1000);
|
|
const second = renderHook(() => useFeedCache("k", (x) => x), {
|
|
wrapper: wrapper(),
|
|
});
|
|
expect(second.result.current.cached).toBeNull();
|
|
});
|
|
|
|
test("a fresh (PUSH) visit ignores the cache", () => {
|
|
sessionStorage.setItem(
|
|
"k",
|
|
JSON.stringify({
|
|
items: [1],
|
|
page: 1,
|
|
hasMore: false,
|
|
scrollY: 0,
|
|
savedAt: Date.now(),
|
|
}),
|
|
);
|
|
function Probe() {
|
|
const { cached } = useFeedCache("k", (x) => x);
|
|
return <p>{cached ? "cached" : "fresh"}</p>;
|
|
}
|
|
function Home() {
|
|
const navigate = useNavigate();
|
|
return (
|
|
<button type="button" onClick={() => navigate("/feed")}>
|
|
go
|
|
</button>
|
|
);
|
|
}
|
|
render(
|
|
<MemoryRouter>
|
|
<Routes>
|
|
<Route path="/" element={<Home />} />
|
|
<Route path="/feed" element={<Probe />} />
|
|
</Routes>
|
|
</MemoryRouter>,
|
|
);
|
|
fireEvent.click(screen.getByText("go"));
|
|
expect(screen.getByText("fresh")).toBeInTheDocument();
|
|
});
|
|
|
|
test("corrupt storage reads as no cache; a full quota doesn't throw", () => {
|
|
sessionStorage.setItem("k", "{");
|
|
const { result } = renderHook(() => useFeedCache("k", (x) => x), {
|
|
wrapper: wrapper(),
|
|
});
|
|
expect(result.current.cached).toBeNull();
|
|
vi.spyOn(Storage.prototype, "setItem").mockImplementation(() => {
|
|
throw new Error("QuotaExceededError");
|
|
});
|
|
expect(() =>
|
|
result.current.saveState({ items: [], page: 1, hasMore: false }, 0)
|
|
).not.toThrow();
|
|
});
|
|
});
|