Deckt die Regression ab, die zuletzt durchrutschte: - Liste scrollen → Tier öffnen → Browser-Zurück → Position bleibt erhalten. - Position überlebt App-Hintergrund/Wiederanzeige (visibilitychange). Läuft im phone-Projekt (390x844, Touch) UND desktop, am echten Scroll-Container .app-main. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
97 lines
4.0 KiB
TypeScript
97 lines
4.0 KiB
TypeScript
/**
|
|
* 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 } from './fixtures'
|
|
|
|
const SCROLLER = '.app-main'
|
|
|
|
async function scrollerTop(page: import('@playwright/test').Page) {
|
|
return page.evaluate((sel) => document.querySelector(sel)?.scrollTop ?? 0, SCROLLER)
|
|
}
|
|
|
|
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 page.evaluate((sel) => {
|
|
const el = document.querySelector(sel)
|
|
if (el) el.scrollTop = Math.min(400, el.scrollHeight - el.clientHeight)
|
|
}, SCROLLER)
|
|
await page.waitForTimeout(150) // rAF-gedrosseltes Speichern abwarten
|
|
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 page.evaluate((sel) => {
|
|
const el = document.querySelector(sel)
|
|
if (el) el.scrollTop = Math.min(400, el.scrollHeight - el.clientHeight)
|
|
}, SCROLLER)
|
|
await page.waitForTimeout(150)
|
|
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)
|
|
})
|
|
})
|