fix(tickets): Tab + Scrollposition überleben Browser-Zurück
Zwei Ursachen, warum es auf der Tickets-Seite nicht ging (am echten Firefox/Chromium gegen die Live-App verifiziert): 1) Tab ging verloren: der gewählte Filter-Tab (z. B. „Geschlossen") war reiner Component-State und sprang nach „Zurück" (Remount) auf „Offen" zurück — also war man in der falschen, kurzen Liste. Jetzt wird der Tab in der Session gemerkt und wiederhergestellt (userPicked startet weiterhin false, damit ?focus=-Deep-Links weiter funktionieren). 2) Scroll-Save-Heuristik zu unzuverlässig: Beim Wegnavigieren schrumpft der Inhalt von .app-main → der Container klemmt auf eine kleinere Position → dieses scroll-Event überschrieb die gemerkte Stelle. Erkennung präzisiert: nur verwerfen, wenn Position UND scrollHeight gleichzeitig SINKEN (= Inhalt geschrumpft/Seitenwechsel). Eine echte Nutzer-Scrollung verkleinert die scrollHeight nie — damit kein fälschliches Verwerfen echter Positionen mehr (vorherige „Höhe geändert"-Heuristik schluckte zu viel). Tests: scroll-restore.spec erweitert (realistisches 2-Schritt-Scrollen + Tickets-Tab überlebt Zurück), desktop+phone grün; vitest 149 grün. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
* 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'
|
||||
import { test, expect, skipUnlessMock, de } from './fixtures'
|
||||
|
||||
const SCROLLER = '.app-main'
|
||||
|
||||
@@ -16,6 +16,26 @@ 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())
|
||||
|
||||
@@ -32,11 +52,7 @@ test.describe('Scroll-Wiederherstellung', () => {
|
||||
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
|
||||
await scrollDown(page, 400)
|
||||
const before = await scrollerTop(page)
|
||||
expect(before).toBeGreaterThan(50)
|
||||
|
||||
@@ -65,11 +81,7 @@ test.describe('Scroll-Wiederherstellung', () => {
|
||||
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)
|
||||
await scrollDown(page, 400)
|
||||
const before = await scrollerTop(page)
|
||||
test.skip(before < 50, 'Liste in diesem Viewport nicht hoch genug')
|
||||
|
||||
@@ -93,4 +105,24 @@ test.describe('Scroll-Wiederherstellung', () => {
|
||||
.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()
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user