From 564104ae231f3de204520295e00b5f8603a16bc4 Mon Sep 17 00:00:00 2001 From: fabi Date: Wed, 1 Jul 2026 19:24:03 +0200 Subject: [PATCH] =?UTF-8?q?test(e2e):=20robustness=20=E2=80=94=20stable=20?= =?UTF-8?q?locators,=20exact=20assertions,=20no=20fixed=20sleep?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ContextSheet: add data-testid="context-sheet". longpress tests targeted the open sheet via the `.translate-y-0` animation class (breaks on any animation refactor) — now target `[data-testid="context-sheet"][aria-modal="true"]`, which is stable and unambiguous vs. the centered LightboxModal (also aria-modal). - toast-on-failure: the like button was `button.filter(hasText:/\d+/).first()`, which could match any digit-bearing button (e.g. the comment count) → use the stable aria-label "Gefällt mir". - config stats: assert the exact user_count (4 = 3 seeded guests + admin) instead of `>= 3` — deterministic after the per-test truncate, catches under/overcount. - offline-network 429 test: replace the fixed 3s waitForTimeout with a poll-until-the-retry-count-stabilizes (faster, and a real storm never stabilizes → the poll fails, which is the intended outcome). Note: reviewed the "config restore not in try/finally" finding — it's a non-issue. The truncate auto-fixture wipes+reseeds the whole config table (and clears the rate limiter) before every test, so config state cannot leak between tests. All affected specs verified green. Co-Authored-By: Claude Opus 4.8 --- e2e/specs/03-feed/toast-on-failure.spec.ts | 5 +++-- e2e/specs/05-admin/config.spec.ts | 6 ++++-- .../08-browser-chaos/offline-network.spec.ts | 19 +++++++++++++++++-- .../09-mobile/gestures-longpress.spec.ts | 19 ++++++++----------- .../src/lib/components/ContextSheet.svelte | 1 + 5 files changed, 33 insertions(+), 17 deletions(-) diff --git a/e2e/specs/03-feed/toast-on-failure.spec.ts b/e2e/specs/03-feed/toast-on-failure.spec.ts index 3d499d5..2228616 100644 --- a/e2e/specs/03-feed/toast-on-failure.spec.ts +++ b/e2e/specs/03-feed/toast-on-failure.spec.ts @@ -41,8 +41,9 @@ test.describe('Feed — error toast on user action failures', () => { const card = page.locator('article').filter({ hasText: author.displayName }).first(); await expect(card).toBeVisible({ timeout: 10_000 }); - // Click the like button in the actions row — first visible match inside the card. - await card.locator('button').filter({ hasText: /\d+/ }).first().click(); + // Click the like button by its stable aria-label (the liker hasn't liked yet). + // Avoids matching a different digit-bearing button (e.g. the comment count). + await card.getByRole('button', { name: 'Gefällt mir' }).click(); // The toast is rendered inside the global Toaster region with aria-live="polite". const toast = page.getByTestId('toast').first(); diff --git a/e2e/specs/05-admin/config.spec.ts b/e2e/specs/05-admin/config.spec.ts index badbfca..92c7f71 100644 --- a/e2e/specs/05-admin/config.spec.ts +++ b/e2e/specs/05-admin/config.spec.ts @@ -63,8 +63,10 @@ test.describe('Admin — stats', () => { await guest('Stat2'); await guest('Stat3'); const stats = await api.getStats(adminToken); - // Three guests + the Admin account auto-created on first admin login = 4 users. - expect(stats.user_count).toBeGreaterThanOrEqual(3); + // Deterministic after the per-test truncate: 3 seeded guests + the Admin account + // (recreated by the adminToken fixture's login) = exactly 4. An exact assertion + // catches undercount/overcount regressions a `>= 3` lower bound would miss. + expect(stats.user_count).toBe(4); expect(typeof stats.disk_total_bytes).toBe('number'); }); }); diff --git a/e2e/specs/08-browser-chaos/offline-network.spec.ts b/e2e/specs/08-browser-chaos/offline-network.spec.ts index 6793da3..653484c 100644 --- a/e2e/specs/08-browser-chaos/offline-network.spec.ts +++ b/e2e/specs/08-browser-chaos/offline-network.spec.ts @@ -72,9 +72,24 @@ test.describe('Browser chaos — network', () => { await signIn(page, g); await page.goto('/feed'); - await page.waitForTimeout(3_000); + + // Wait until the retry count stops climbing instead of sleeping a fixed 3s: a + // well-behaved client surfaces the 429 and stops, so the count settles quickly; + // a retry storm would keep incrementing and never stabilize (→ this poll times + // out and the test fails, which is the outcome we want). + let prev = -1; + await expect + .poll( + () => { + const stable = attempts === prev; + prev = attempts; + return stable; + }, + { timeout: 8_000, intervals: [300] } + ) + .toBe(true); // Sanity: client did not hammer the endpoint > a few times under throttle. - expect(attempts).toBeLessThan(15); + expect(attempts, `retry attempts=${attempts}`).toBeLessThan(15); }); }); diff --git a/e2e/specs/09-mobile/gestures-longpress.spec.ts b/e2e/specs/09-mobile/gestures-longpress.spec.ts index ab64e08..67bdba7 100644 --- a/e2e/specs/09-mobile/gestures-longpress.spec.ts +++ b/e2e/specs/09-mobile/gestures-longpress.spec.ts @@ -45,13 +45,11 @@ test.describe('Mobile — long-press gesture', () => { await longPress(page, card, 600); - // The ContextSheet renders a dialog with role="dialog" + aria-modal="true". - // Multiple sheets (UploadSheet, ContextSheet) may be in the DOM — match the - // one that actually has aria-modal=true (i.e. the open one). - // ContextSheet is always mounted (it just translates off-screen when closed). - // Match the OPEN state by the `translate-y-0` class the component applies - // when `open === true`. - const sheet = page.locator('[role="dialog"][aria-modal="true"].translate-y-0'); + // The ContextSheet is always mounted (it translates off-screen when closed). + // Target it by its stable data-testid, gated on aria-modal="true" which the + // component sets only while open — unambiguous vs. the centered LightboxModal + // (which also has aria-modal) and independent of the animation classes. + const sheet = page.locator('[data-testid="context-sheet"][aria-modal="true"]'); await expect(sheet).toBeVisible({ timeout: 2_000 }); await expect(sheet.getByRole('button', { name: /abbrechen/i })).toBeVisible(); }); @@ -68,10 +66,9 @@ test.describe('Mobile — long-press gesture', () => { // Simulate a short press (200 ms — well under the 500 ms threshold). await longPress(page, card, 200); - // Within 1 s, no aria-modal=true dialog should be open (the ContextSheet - // is "open" only when its aria-modal flag is true). - // The ContextSheet stays mounted but `translate-y-0` is only set when open. - await expect(page.locator('[role="dialog"][aria-modal="true"].translate-y-0')).toHaveCount(0, { timeout: 1_000 }); + // Within 1 s, the ContextSheet must not be open (aria-modal is set only when + // open). A quick tap opens the lightbox instead, which is a different element. + await expect(page.locator('[data-testid="context-sheet"][aria-modal="true"]')).toHaveCount(0, { timeout: 1_000 }); }); test('long-press suppresses the click that lands at pointerup (no double-open of lightbox)', async ({ page, guest, signIn }) => { diff --git a/frontend/src/lib/components/ContextSheet.svelte b/frontend/src/lib/components/ContextSheet.svelte index 07562ae..f5af0bd 100644 --- a/frontend/src/lib/components/ContextSheet.svelte +++ b/frontend/src/lib/components/ContextSheet.svelte @@ -108,6 +108,7 @@ aria-hidden={!open} inert={!open} tabindex="-1" + data-testid="context-sheet" >