Files
kochwas/tests/unit/scroll-restore.test.ts

104 lines
3.3 KiB
TypeScript
Raw Normal View History

// @vitest-environment jsdom
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
import { recordScroll, restoreScroll } from '../../src/lib/client/scroll-restore';
const STORAGE_KEY = 'kochwas:scroll';
function setScrollY(y: number) {
Object.defineProperty(window, 'scrollY', { value: y, configurable: true });
}
function setDocHeight(h: number) {
Object.defineProperty(document.documentElement, 'scrollHeight', {
value: h,
configurable: true
});
}
function setViewportHeight(h: number) {
Object.defineProperty(window, 'innerHeight', { value: h, configurable: true });
}
describe('scroll-restore', () => {
let scrollToSpy: ReturnType<typeof vi.spyOn>;
beforeEach(() => {
sessionStorage.clear();
setScrollY(0);
setViewportHeight(800);
setDocHeight(800);
scrollToSpy = vi
.spyOn(window, 'scrollTo')
.mockImplementation(() => undefined as unknown as void);
window.history.replaceState({}, '', '/wishlist');
});
afterEach(() => {
scrollToSpy.mockRestore();
vi.useRealTimers();
});
it('records scrollY keyed by pathname+search', () => {
setScrollY(1200);
recordScroll();
const map = JSON.parse(sessionStorage.getItem(STORAGE_KEY) ?? '{}');
expect(map['/wishlist']).toBe(1200);
});
it('keeps separate entries per URL', () => {
setScrollY(500);
recordScroll();
window.history.replaceState({}, '', '/?q=hi');
setScrollY(900);
recordScroll();
const map = JSON.parse(sessionStorage.getItem(STORAGE_KEY) ?? '{}');
expect(map['/wishlist']).toBe(500);
expect(map['/?q=hi']).toBe(900);
});
it('skips restore for non-popstate navigation', () => {
sessionStorage.setItem(STORAGE_KEY, JSON.stringify({ '/wishlist': 1000 }));
setDocHeight(5000);
restoreScroll('link');
expect(scrollToSpy).not.toHaveBeenCalled();
});
it('skips restore when no entry stored', () => {
setDocHeight(5000);
restoreScroll('popstate');
expect(scrollToSpy).not.toHaveBeenCalled();
});
it('skips restore for trivial scrollY (noise)', () => {
sessionStorage.setItem(STORAGE_KEY, JSON.stringify({ '/wishlist': 10 }));
setDocHeight(5000);
restoreScroll('popstate');
expect(scrollToSpy).not.toHaveBeenCalled();
});
it('scrolls immediately when document is already tall enough', async () => {
sessionStorage.setItem(STORAGE_KEY, JSON.stringify({ '/wishlist': 1000 }));
setDocHeight(5000);
restoreScroll('popstate');
// rAF resolves on next microtask in jsdom
await new Promise((r) => requestAnimationFrame(() => r(null)));
expect(scrollToSpy).toHaveBeenCalledWith({ top: 1000, left: 0, behavior: 'instant' });
});
it('waits via rAF until document grows tall enough', async () => {
sessionStorage.setItem(STORAGE_KEY, JSON.stringify({ '/wishlist': 1500 }));
// Initially short — would clamp.
setDocHeight(900);
restoreScroll('popstate');
await new Promise((r) => requestAnimationFrame(() => r(null)));
expect(scrollToSpy).not.toHaveBeenCalled();
// Simulate async data loading and document growing.
setDocHeight(3000);
// Allow rAF to fire again.
await new Promise((r) => requestAnimationFrame(() => r(null)));
expect(scrollToSpy).toHaveBeenCalledWith({ top: 1500, left: 0, behavior: 'instant' });
});
});