/** * Scroll-Wiederherstellung (useScrollRestoration im AppShell). * * Regression: Liste runterscrollen → Eintrag öffnen → Browser-Zurück → die Position * muss erhalten bleiben (vorher sprang sie auf 0, weil beim Wegnavigieren der kollabierende * Inhalt ein scroll→0 auslöste, das die gemerkte Position überschrieb). * * Gescrollt wird der Container `.app-main` (nicht das Fenster). Läuft in beiden Projekten * (desktop + phone) — die Rennmausliste ist in beiden Viewports hoch genug zum Scrollen. */ import { test, expect, skipUnlessMock, de } from './fixtures' const SCROLLER = '.app-main' async function scrollerTop(page: import('@playwright/test').Page) { return page.evaluate((sel) => document.querySelector(sel)?.scrollTop ?? 0, SCROLLER) } /** * Realistisch runterscrollen: ein Warm-up-Schritt (synchronisiert die gemerkte Höhe nach * dem ersten Render) + der eigentliche Schritt — wie echtes Scrollen, das viele Events feuert. */ async function scrollDown(page: import('@playwright/test').Page, target: number) { await page.evaluate((sel) => { const el = document.querySelector(sel) if (el) el.scrollTop = 60 }, SCROLLER) await page.waitForTimeout(80) await page.evaluate( ([sel, t]) => { const el = document.querySelector(sel as string) if (el) el.scrollTop = Math.min(t as number, el.scrollHeight - el.clientHeight) }, [SCROLLER, target] as const, ) await page.waitForTimeout(150) // rAF-gedrosseltes Speichern abwarten } test.describe('Scroll-Wiederherstellung', () => { test.beforeEach(() => skipUnlessMock()) test('Rennmausliste: Position überlebt Detail-Öffnen + Browser-Zurück', async ({ page }) => { await page.goto('/rennmaeuse') // Warten bis Tierkarten-Links da sind. await expect(page.locator('a[href^="/rennmaeuse/"]').first()).toBeVisible() // Container muss scrollbar sein, sonst ist der Test sinnlos. const scrollable = await page.evaluate((sel) => { const el = document.querySelector(sel) return el ? el.scrollHeight > el.clientHeight + 50 : false }, SCROLLER) expect(scrollable, 'Liste muss scrollbar sein').toBeTruthy() // Ein Stück runterscrollen. await scrollDown(page, 400) const before = await scrollerTop(page) expect(before).toBeGreaterThan(50) // Einen AKTUELL SICHTBAREN Tier-Link öffnen (kein Auto-Scroll, wie ein echter Klick). const href = await page.evaluate(() => { for (const a of document.querySelectorAll('a[href^="/rennmaeuse/"]')) { const r = a.getBoundingClientRect() if (r.top >= 0 && r.bottom <= window.innerHeight && a.getAttribute('href') !== '/rennmaeuse/neu') { ;(a as HTMLElement).click() return a.getAttribute('href') } } return null }) expect(href).toBeTruthy() await expect(page).toHaveURL(/\/rennmaeuse\/[^/]+$/) // Zurück — Position muss (nahezu) wiederhergestellt sein. await page.goBack() await expect(page.locator('a[href^="/rennmaeuse/"]').first()).toBeVisible() await expect .poll(async () => scrollerTop(page), { timeout: 4000, message: 'Scrollposition wiederhergestellt' }) .toBeGreaterThan(before - 30) }) test('Rennmausliste: Position überlebt App-Hintergrund (visibilitychange)', async ({ page }) => { await page.goto('/rennmaeuse') await expect(page.locator('a[href^="/rennmaeuse/"]').first()).toBeVisible() await scrollDown(page, 400) const before = await scrollerTop(page) test.skip(before < 50, 'Liste in diesem Viewport nicht hoch genug') // App in den Hintergrund (Handy-Sperre) und zurück simulieren. await page.evaluate(() => { Object.defineProperty(document, 'visibilityState', { configurable: true, get: () => 'hidden' }) document.dispatchEvent(new Event('visibilitychange')) }) // Inhalt „springt" (wie beim Aufwachen) künstlich nach oben … await page.evaluate((sel) => { const el = document.querySelector(sel) if (el) el.scrollTop = 0 }, SCROLLER) await page.evaluate(() => { Object.defineProperty(document, 'visibilityState', { configurable: true, get: () => 'visible' }) document.dispatchEvent(new Event('visibilitychange')) }) // … und muss wiederhergestellt werden. await expect .poll(async () => scrollerTop(page), { timeout: 4000 }) .toBeGreaterThan(before - 30) }) test('Tickets: gewählter Tab überlebt Browser-Zurück (nicht zurück auf „Offen")', async ({ page }) => { const tt = de.feedback.tickets await page.goto('/hilfe/tickets') // In die „Geschlossen"-Ansicht wechseln. await page.locator('.tickets-filter').filter({ hasText: tt.filters.closed }).click() await expect( page.locator('.tickets-filter--active').filter({ hasText: tt.filters.closed }), ).toBeVisible() // Über die Brotkrümel weg und per Browser-Zurück wieder her. await page.locator('.tickets-breadcrumb a[href="/hilfe"]').click() await expect(page).toHaveURL(/\/hilfe$/) await page.goBack() // Tab muss weiterhin „Geschlossen" sein (vorher sprang er zurück auf „Offen"). await expect( page.locator('.tickets-filter--active').filter({ hasText: tt.filters.closed }), ).toBeVisible() }) })