fix(nav): Scroll-Position bei Browser-Back robust wiederherstellen
Pages, die ihre Daten in onMount per fetch laden (Home, Wunschliste,
Einkaufsliste), waren bei popstate-Navigation kaputt: SvelteKit ruft
scrollTo() synchron nach Mount, aber die Listen sind dann noch leer
und das Dokument zu kurz — der Browser clamped auf 0.
Neuer Helper src/lib/client/scroll-restore.ts merkt scrollY pro URL in
sessionStorage (beforeNavigate) und stellt sie bei popstate per rAF-
Polling wieder her, sobald document.scrollHeight gross genug ist
(Hard-Budget 1.5s, danach best-effort scrollTo).
Layout ruft die zwei Helper im beforeNavigate / afterNavigate. Pages
mit SSR-Daten (z. B. /recipes) bleiben unbeeinflusst — dort matcht
unser Wert SvelteKits eigenen scrollTo bereits beim ersten Frame.
Tests: 7 neue Unit-Tests in tests/unit/scroll-restore.test.ts decken
Recording, Pro-URL-Trennung, Skip fuer Forward-Nav, sofortiges und
verzoegertes Restore ab.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 09:18:00 +02:00
|
|
|
// @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 });
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-22 09:28:20 +02:00
|
|
|
function url(path: string): URL {
|
|
|
|
|
return new URL(path, 'https://example.test');
|
|
|
|
|
}
|
|
|
|
|
|
fix(nav): Scroll-Position bei Browser-Back robust wiederherstellen
Pages, die ihre Daten in onMount per fetch laden (Home, Wunschliste,
Einkaufsliste), waren bei popstate-Navigation kaputt: SvelteKit ruft
scrollTo() synchron nach Mount, aber die Listen sind dann noch leer
und das Dokument zu kurz — der Browser clamped auf 0.
Neuer Helper src/lib/client/scroll-restore.ts merkt scrollY pro URL in
sessionStorage (beforeNavigate) und stellt sie bei popstate per rAF-
Polling wieder her, sobald document.scrollHeight gross genug ist
(Hard-Budget 1.5s, danach best-effort scrollTo).
Layout ruft die zwei Helper im beforeNavigate / afterNavigate. Pages
mit SSR-Daten (z. B. /recipes) bleiben unbeeinflusst — dort matcht
unser Wert SvelteKits eigenen scrollTo bereits beim ersten Frame.
Tests: 7 neue Unit-Tests in tests/unit/scroll-restore.test.ts decken
Recording, Pro-URL-Trennung, Skip fuer Forward-Nav, sofortiges und
verzoegertes Restore ab.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 09:18:00 +02:00
|
|
|
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);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
|
scrollToSpy.mockRestore();
|
|
|
|
|
vi.useRealTimers();
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-22 09:28:20 +02:00
|
|
|
it('records scrollY keyed by from-url pathname+search', () => {
|
fix(nav): Scroll-Position bei Browser-Back robust wiederherstellen
Pages, die ihre Daten in onMount per fetch laden (Home, Wunschliste,
Einkaufsliste), waren bei popstate-Navigation kaputt: SvelteKit ruft
scrollTo() synchron nach Mount, aber die Listen sind dann noch leer
und das Dokument zu kurz — der Browser clamped auf 0.
Neuer Helper src/lib/client/scroll-restore.ts merkt scrollY pro URL in
sessionStorage (beforeNavigate) und stellt sie bei popstate per rAF-
Polling wieder her, sobald document.scrollHeight gross genug ist
(Hard-Budget 1.5s, danach best-effort scrollTo).
Layout ruft die zwei Helper im beforeNavigate / afterNavigate. Pages
mit SSR-Daten (z. B. /recipes) bleiben unbeeinflusst — dort matcht
unser Wert SvelteKits eigenen scrollTo bereits beim ersten Frame.
Tests: 7 neue Unit-Tests in tests/unit/scroll-restore.test.ts decken
Recording, Pro-URL-Trennung, Skip fuer Forward-Nav, sofortiges und
verzoegertes Restore ab.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 09:18:00 +02:00
|
|
|
setScrollY(1200);
|
2026-04-22 09:28:20 +02:00
|
|
|
recordScroll(url('/wishlist'));
|
fix(nav): Scroll-Position bei Browser-Back robust wiederherstellen
Pages, die ihre Daten in onMount per fetch laden (Home, Wunschliste,
Einkaufsliste), waren bei popstate-Navigation kaputt: SvelteKit ruft
scrollTo() synchron nach Mount, aber die Listen sind dann noch leer
und das Dokument zu kurz — der Browser clamped auf 0.
Neuer Helper src/lib/client/scroll-restore.ts merkt scrollY pro URL in
sessionStorage (beforeNavigate) und stellt sie bei popstate per rAF-
Polling wieder her, sobald document.scrollHeight gross genug ist
(Hard-Budget 1.5s, danach best-effort scrollTo).
Layout ruft die zwei Helper im beforeNavigate / afterNavigate. Pages
mit SSR-Daten (z. B. /recipes) bleiben unbeeinflusst — dort matcht
unser Wert SvelteKits eigenen scrollTo bereits beim ersten Frame.
Tests: 7 neue Unit-Tests in tests/unit/scroll-restore.test.ts decken
Recording, Pro-URL-Trennung, Skip fuer Forward-Nav, sofortiges und
verzoegertes Restore ab.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 09:18:00 +02:00
|
|
|
const map = JSON.parse(sessionStorage.getItem(STORAGE_KEY) ?? '{}');
|
|
|
|
|
expect(map['/wishlist']).toBe(1200);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('keeps separate entries per URL', () => {
|
|
|
|
|
setScrollY(500);
|
2026-04-22 09:28:20 +02:00
|
|
|
recordScroll(url('/wishlist'));
|
fix(nav): Scroll-Position bei Browser-Back robust wiederherstellen
Pages, die ihre Daten in onMount per fetch laden (Home, Wunschliste,
Einkaufsliste), waren bei popstate-Navigation kaputt: SvelteKit ruft
scrollTo() synchron nach Mount, aber die Listen sind dann noch leer
und das Dokument zu kurz — der Browser clamped auf 0.
Neuer Helper src/lib/client/scroll-restore.ts merkt scrollY pro URL in
sessionStorage (beforeNavigate) und stellt sie bei popstate per rAF-
Polling wieder her, sobald document.scrollHeight gross genug ist
(Hard-Budget 1.5s, danach best-effort scrollTo).
Layout ruft die zwei Helper im beforeNavigate / afterNavigate. Pages
mit SSR-Daten (z. B. /recipes) bleiben unbeeinflusst — dort matcht
unser Wert SvelteKits eigenen scrollTo bereits beim ersten Frame.
Tests: 7 neue Unit-Tests in tests/unit/scroll-restore.test.ts decken
Recording, Pro-URL-Trennung, Skip fuer Forward-Nav, sofortiges und
verzoegertes Restore ab.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 09:18:00 +02:00
|
|
|
setScrollY(900);
|
2026-04-22 09:28:20 +02:00
|
|
|
recordScroll(url('/?q=hi'));
|
fix(nav): Scroll-Position bei Browser-Back robust wiederherstellen
Pages, die ihre Daten in onMount per fetch laden (Home, Wunschliste,
Einkaufsliste), waren bei popstate-Navigation kaputt: SvelteKit ruft
scrollTo() synchron nach Mount, aber die Listen sind dann noch leer
und das Dokument zu kurz — der Browser clamped auf 0.
Neuer Helper src/lib/client/scroll-restore.ts merkt scrollY pro URL in
sessionStorage (beforeNavigate) und stellt sie bei popstate per rAF-
Polling wieder her, sobald document.scrollHeight gross genug ist
(Hard-Budget 1.5s, danach best-effort scrollTo).
Layout ruft die zwei Helper im beforeNavigate / afterNavigate. Pages
mit SSR-Daten (z. B. /recipes) bleiben unbeeinflusst — dort matcht
unser Wert SvelteKits eigenen scrollTo bereits beim ersten Frame.
Tests: 7 neue Unit-Tests in tests/unit/scroll-restore.test.ts decken
Recording, Pro-URL-Trennung, Skip fuer Forward-Nav, sofortiges und
verzoegertes Restore ab.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 09:18:00 +02:00
|
|
|
const map = JSON.parse(sessionStorage.getItem(STORAGE_KEY) ?? '{}');
|
|
|
|
|
expect(map['/wishlist']).toBe(500);
|
|
|
|
|
expect(map['/?q=hi']).toBe(900);
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-22 09:28:20 +02:00
|
|
|
it('does not overwrite a stored URL when called with a different from-url', () => {
|
|
|
|
|
// This is the regression: on popstate, location.pathname is already
|
|
|
|
|
// the new URL. Recording must use nav.from.url (the page being left),
|
|
|
|
|
// not location, or we wipe the destination's saved scrollY.
|
|
|
|
|
setScrollY(500);
|
|
|
|
|
recordScroll(url('/'));
|
|
|
|
|
setScrollY(0);
|
|
|
|
|
recordScroll(url('/recipes/1'));
|
|
|
|
|
const map = JSON.parse(sessionStorage.getItem(STORAGE_KEY) ?? '{}');
|
|
|
|
|
expect(map['/']).toBe(500);
|
|
|
|
|
expect(map['/recipes/1']).toBe(0);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('skips when from-url is missing', () => {
|
|
|
|
|
setScrollY(900);
|
|
|
|
|
recordScroll(null);
|
|
|
|
|
expect(sessionStorage.getItem(STORAGE_KEY)).toBeNull();
|
|
|
|
|
});
|
|
|
|
|
|
fix(nav): Scroll-Position bei Browser-Back robust wiederherstellen
Pages, die ihre Daten in onMount per fetch laden (Home, Wunschliste,
Einkaufsliste), waren bei popstate-Navigation kaputt: SvelteKit ruft
scrollTo() synchron nach Mount, aber die Listen sind dann noch leer
und das Dokument zu kurz — der Browser clamped auf 0.
Neuer Helper src/lib/client/scroll-restore.ts merkt scrollY pro URL in
sessionStorage (beforeNavigate) und stellt sie bei popstate per rAF-
Polling wieder her, sobald document.scrollHeight gross genug ist
(Hard-Budget 1.5s, danach best-effort scrollTo).
Layout ruft die zwei Helper im beforeNavigate / afterNavigate. Pages
mit SSR-Daten (z. B. /recipes) bleiben unbeeinflusst — dort matcht
unser Wert SvelteKits eigenen scrollTo bereits beim ersten Frame.
Tests: 7 neue Unit-Tests in tests/unit/scroll-restore.test.ts decken
Recording, Pro-URL-Trennung, Skip fuer Forward-Nav, sofortiges und
verzoegertes Restore ab.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 09:18:00 +02:00
|
|
|
it('skips restore for non-popstate navigation', () => {
|
|
|
|
|
sessionStorage.setItem(STORAGE_KEY, JSON.stringify({ '/wishlist': 1000 }));
|
|
|
|
|
setDocHeight(5000);
|
2026-04-22 09:28:20 +02:00
|
|
|
restoreScroll('link', url('/wishlist'));
|
|
|
|
|
expect(scrollToSpy).not.toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('skips restore when to-url is missing', () => {
|
|
|
|
|
sessionStorage.setItem(STORAGE_KEY, JSON.stringify({ '/wishlist': 1000 }));
|
|
|
|
|
restoreScroll('popstate', null);
|
fix(nav): Scroll-Position bei Browser-Back robust wiederherstellen
Pages, die ihre Daten in onMount per fetch laden (Home, Wunschliste,
Einkaufsliste), waren bei popstate-Navigation kaputt: SvelteKit ruft
scrollTo() synchron nach Mount, aber die Listen sind dann noch leer
und das Dokument zu kurz — der Browser clamped auf 0.
Neuer Helper src/lib/client/scroll-restore.ts merkt scrollY pro URL in
sessionStorage (beforeNavigate) und stellt sie bei popstate per rAF-
Polling wieder her, sobald document.scrollHeight gross genug ist
(Hard-Budget 1.5s, danach best-effort scrollTo).
Layout ruft die zwei Helper im beforeNavigate / afterNavigate. Pages
mit SSR-Daten (z. B. /recipes) bleiben unbeeinflusst — dort matcht
unser Wert SvelteKits eigenen scrollTo bereits beim ersten Frame.
Tests: 7 neue Unit-Tests in tests/unit/scroll-restore.test.ts decken
Recording, Pro-URL-Trennung, Skip fuer Forward-Nav, sofortiges und
verzoegertes Restore ab.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 09:18:00 +02:00
|
|
|
expect(scrollToSpy).not.toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('skips restore when no entry stored', () => {
|
|
|
|
|
setDocHeight(5000);
|
2026-04-22 09:28:20 +02:00
|
|
|
restoreScroll('popstate', url('/wishlist'));
|
fix(nav): Scroll-Position bei Browser-Back robust wiederherstellen
Pages, die ihre Daten in onMount per fetch laden (Home, Wunschliste,
Einkaufsliste), waren bei popstate-Navigation kaputt: SvelteKit ruft
scrollTo() synchron nach Mount, aber die Listen sind dann noch leer
und das Dokument zu kurz — der Browser clamped auf 0.
Neuer Helper src/lib/client/scroll-restore.ts merkt scrollY pro URL in
sessionStorage (beforeNavigate) und stellt sie bei popstate per rAF-
Polling wieder her, sobald document.scrollHeight gross genug ist
(Hard-Budget 1.5s, danach best-effort scrollTo).
Layout ruft die zwei Helper im beforeNavigate / afterNavigate. Pages
mit SSR-Daten (z. B. /recipes) bleiben unbeeinflusst — dort matcht
unser Wert SvelteKits eigenen scrollTo bereits beim ersten Frame.
Tests: 7 neue Unit-Tests in tests/unit/scroll-restore.test.ts decken
Recording, Pro-URL-Trennung, Skip fuer Forward-Nav, sofortiges und
verzoegertes Restore ab.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 09:18:00 +02:00
|
|
|
expect(scrollToSpy).not.toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('skips restore for trivial scrollY (noise)', () => {
|
|
|
|
|
sessionStorage.setItem(STORAGE_KEY, JSON.stringify({ '/wishlist': 10 }));
|
|
|
|
|
setDocHeight(5000);
|
2026-04-22 09:28:20 +02:00
|
|
|
restoreScroll('popstate', url('/wishlist'));
|
fix(nav): Scroll-Position bei Browser-Back robust wiederherstellen
Pages, die ihre Daten in onMount per fetch laden (Home, Wunschliste,
Einkaufsliste), waren bei popstate-Navigation kaputt: SvelteKit ruft
scrollTo() synchron nach Mount, aber die Listen sind dann noch leer
und das Dokument zu kurz — der Browser clamped auf 0.
Neuer Helper src/lib/client/scroll-restore.ts merkt scrollY pro URL in
sessionStorage (beforeNavigate) und stellt sie bei popstate per rAF-
Polling wieder her, sobald document.scrollHeight gross genug ist
(Hard-Budget 1.5s, danach best-effort scrollTo).
Layout ruft die zwei Helper im beforeNavigate / afterNavigate. Pages
mit SSR-Daten (z. B. /recipes) bleiben unbeeinflusst — dort matcht
unser Wert SvelteKits eigenen scrollTo bereits beim ersten Frame.
Tests: 7 neue Unit-Tests in tests/unit/scroll-restore.test.ts decken
Recording, Pro-URL-Trennung, Skip fuer Forward-Nav, sofortiges und
verzoegertes Restore ab.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 09:18:00 +02:00
|
|
|
expect(scrollToSpy).not.toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('scrolls immediately when document is already tall enough', async () => {
|
|
|
|
|
sessionStorage.setItem(STORAGE_KEY, JSON.stringify({ '/wishlist': 1000 }));
|
|
|
|
|
setDocHeight(5000);
|
2026-04-22 09:28:20 +02:00
|
|
|
restoreScroll('popstate', url('/wishlist'));
|
fix(nav): Scroll-Position bei Browser-Back robust wiederherstellen
Pages, die ihre Daten in onMount per fetch laden (Home, Wunschliste,
Einkaufsliste), waren bei popstate-Navigation kaputt: SvelteKit ruft
scrollTo() synchron nach Mount, aber die Listen sind dann noch leer
und das Dokument zu kurz — der Browser clamped auf 0.
Neuer Helper src/lib/client/scroll-restore.ts merkt scrollY pro URL in
sessionStorage (beforeNavigate) und stellt sie bei popstate per rAF-
Polling wieder her, sobald document.scrollHeight gross genug ist
(Hard-Budget 1.5s, danach best-effort scrollTo).
Layout ruft die zwei Helper im beforeNavigate / afterNavigate. Pages
mit SSR-Daten (z. B. /recipes) bleiben unbeeinflusst — dort matcht
unser Wert SvelteKits eigenen scrollTo bereits beim ersten Frame.
Tests: 7 neue Unit-Tests in tests/unit/scroll-restore.test.ts decken
Recording, Pro-URL-Trennung, Skip fuer Forward-Nav, sofortiges und
verzoegertes Restore ab.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 09:18:00 +02:00
|
|
|
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 }));
|
|
|
|
|
setDocHeight(900);
|
2026-04-22 09:28:20 +02:00
|
|
|
restoreScroll('popstate', url('/wishlist'));
|
fix(nav): Scroll-Position bei Browser-Back robust wiederherstellen
Pages, die ihre Daten in onMount per fetch laden (Home, Wunschliste,
Einkaufsliste), waren bei popstate-Navigation kaputt: SvelteKit ruft
scrollTo() synchron nach Mount, aber die Listen sind dann noch leer
und das Dokument zu kurz — der Browser clamped auf 0.
Neuer Helper src/lib/client/scroll-restore.ts merkt scrollY pro URL in
sessionStorage (beforeNavigate) und stellt sie bei popstate per rAF-
Polling wieder her, sobald document.scrollHeight gross genug ist
(Hard-Budget 1.5s, danach best-effort scrollTo).
Layout ruft die zwei Helper im beforeNavigate / afterNavigate. Pages
mit SSR-Daten (z. B. /recipes) bleiben unbeeinflusst — dort matcht
unser Wert SvelteKits eigenen scrollTo bereits beim ersten Frame.
Tests: 7 neue Unit-Tests in tests/unit/scroll-restore.test.ts decken
Recording, Pro-URL-Trennung, Skip fuer Forward-Nav, sofortiges und
verzoegertes Restore ab.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 09:18:00 +02:00
|
|
|
await new Promise((r) => requestAnimationFrame(() => r(null)));
|
|
|
|
|
expect(scrollToSpy).not.toHaveBeenCalled();
|
|
|
|
|
|
|
|
|
|
setDocHeight(3000);
|
|
|
|
|
await new Promise((r) => requestAnimationFrame(() => r(null)));
|
|
|
|
|
expect(scrollToSpy).toHaveBeenCalledWith({ top: 1500, left: 0, behavior: 'instant' });
|
|
|
|
|
});
|
|
|
|
|
});
|