test(e2e): robustness — stable locators, exact assertions, no fixed sleep
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -41,8 +41,9 @@ test.describe('Feed — error toast on user action failures', () => {
|
|||||||
const card = page.locator('article').filter({ hasText: author.displayName }).first();
|
const card = page.locator('article').filter({ hasText: author.displayName }).first();
|
||||||
await expect(card).toBeVisible({ timeout: 10_000 });
|
await expect(card).toBeVisible({ timeout: 10_000 });
|
||||||
|
|
||||||
// Click the like button in the actions row — first visible match inside the card.
|
// Click the like button by its stable aria-label (the liker hasn't liked yet).
|
||||||
await card.locator('button').filter({ hasText: /\d+/ }).first().click();
|
// 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".
|
// The toast is rendered inside the global Toaster region with aria-live="polite".
|
||||||
const toast = page.getByTestId('toast').first();
|
const toast = page.getByTestId('toast').first();
|
||||||
|
|||||||
@@ -63,8 +63,10 @@ test.describe('Admin — stats', () => {
|
|||||||
await guest('Stat2');
|
await guest('Stat2');
|
||||||
await guest('Stat3');
|
await guest('Stat3');
|
||||||
const stats = await api.getStats(adminToken);
|
const stats = await api.getStats(adminToken);
|
||||||
// Three guests + the Admin account auto-created on first admin login = 4 users.
|
// Deterministic after the per-test truncate: 3 seeded guests + the Admin account
|
||||||
expect(stats.user_count).toBeGreaterThanOrEqual(3);
|
// (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');
|
expect(typeof stats.disk_total_bytes).toBe('number');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -72,9 +72,24 @@ test.describe('Browser chaos — network', () => {
|
|||||||
|
|
||||||
await signIn(page, g);
|
await signIn(page, g);
|
||||||
await page.goto('/feed');
|
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.
|
// Sanity: client did not hammer the endpoint > a few times under throttle.
|
||||||
expect(attempts).toBeLessThan(15);
|
expect(attempts, `retry attempts=${attempts}`).toBeLessThan(15);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -45,13 +45,11 @@ test.describe('Mobile — long-press gesture', () => {
|
|||||||
|
|
||||||
await longPress(page, card, 600);
|
await longPress(page, card, 600);
|
||||||
|
|
||||||
// The ContextSheet renders a dialog with role="dialog" + aria-modal="true".
|
// The ContextSheet is always mounted (it translates off-screen when closed).
|
||||||
// Multiple sheets (UploadSheet, ContextSheet) may be in the DOM — match the
|
// Target it by its stable data-testid, gated on aria-modal="true" which the
|
||||||
// one that actually has aria-modal=true (i.e. the open one).
|
// component sets only while open — unambiguous vs. the centered LightboxModal
|
||||||
// ContextSheet is always mounted (it just translates off-screen when closed).
|
// (which also has aria-modal) and independent of the animation classes.
|
||||||
// Match the OPEN state by the `translate-y-0` class the component applies
|
const sheet = page.locator('[data-testid="context-sheet"][aria-modal="true"]');
|
||||||
// when `open === true`.
|
|
||||||
const sheet = page.locator('[role="dialog"][aria-modal="true"].translate-y-0');
|
|
||||||
await expect(sheet).toBeVisible({ timeout: 2_000 });
|
await expect(sheet).toBeVisible({ timeout: 2_000 });
|
||||||
await expect(sheet.getByRole('button', { name: /abbrechen/i })).toBeVisible();
|
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).
|
// Simulate a short press (200 ms — well under the 500 ms threshold).
|
||||||
await longPress(page, card, 200);
|
await longPress(page, card, 200);
|
||||||
|
|
||||||
// Within 1 s, no aria-modal=true dialog should be open (the ContextSheet
|
// Within 1 s, the ContextSheet must not be open (aria-modal is set only when
|
||||||
// is "open" only when its aria-modal flag is true).
|
// open). A quick tap opens the lightbox instead, which is a different element.
|
||||||
// The ContextSheet stays mounted but `translate-y-0` is only set when open.
|
await expect(page.locator('[data-testid="context-sheet"][aria-modal="true"]')).toHaveCount(0, { timeout: 1_000 });
|
||||||
await expect(page.locator('[role="dialog"][aria-modal="true"].translate-y-0')).toHaveCount(0, { timeout: 1_000 });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('long-press suppresses the click that lands at pointerup (no double-open of lightbox)', async ({ page, guest, signIn }) => {
|
test('long-press suppresses the click that lands at pointerup (no double-open of lightbox)', async ({ page, guest, signIn }) => {
|
||||||
|
|||||||
@@ -108,6 +108,7 @@
|
|||||||
aria-hidden={!open}
|
aria-hidden={!open}
|
||||||
inert={!open}
|
inert={!open}
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
|
data-testid="context-sheet"
|
||||||
>
|
>
|
||||||
<div class="flex justify-center pt-3 pb-1">
|
<div class="flex justify-center pt-3 pb-1">
|
||||||
<div class="h-1 w-10 rounded-full bg-gray-300 dark:bg-gray-600"></div>
|
<div class="h-1 w-10 rounded-full bg-gray-300 dark:bg-gray-600"></div>
|
||||||
|
|||||||
Reference in New Issue
Block a user