Files
EventSnap/e2e/specs/09-mobile/viewport-reflow.spec.ts
MechaCat02 e42d8a92a1 feat(e2e): Playwright suite — 134 tests across 9 spec areas + UA matrix
Adds an end-to-end Playwright test suite under e2e/ that spins up an
isolated docker-compose stack (Postgres :55432, Caddy :3101, backend with
EVENTSNAP_TEST_MODE=1, SvelteKit adapter-node frontend) and exercises the
SvelteKit app against the real Rust backend.

Phase 1 — happy paths covering every documented USER_JOURNEYS.md flow:
  01-auth/      join, recover, admin login, leave event, PIN lockout
  02-upload/    gallery picker (API path), rate-limit + admin toggle
  03-feed/      like/comment SSE, filters, SSE reconnect on visibility
  04-host/      event lock API, ban/unban, promote
  05-admin/     config validation, foundational authz guards, stats
  06-export/    /export status + download stub
  __smoke/      cross-UA happy-path (runs on every UA project)

Phase 2 — adversarial + browser chaos:
  07-adversarial/  XSS payloads (6 × display name path), SQLi shapes,
                   length / encoding / RTL override / NUL byte;
                   file-upload boundaries (ELF body claimed as JPEG,
                   oversize vs max_image_size_mb, zero-byte, NUL
                   filename, path-traversal, SVG-with-script);
                   JWT alg:none, signature/payload tamper, expired
                   session, PIN brute-force (serial + parallel),
                   admin password brute-force; deep authz (cross-user
                   delete, banned user across like/comment/feed-read,
                   host→admin escalation); small-scale DDoS (20× /join,
                   10MB comment body, 10 concurrent SSE).
  08-browser-chaos/ localStorage / sessionStorage / cookie purge,
                    IndexedDB drop mid-session, offline → reconnect,
                    slow-3G, 503 flakes, 429 with no retry storm,
                    multi-tab same/different user, no-JS, hostile CSS,
                    clock skew ±1h / -2d, localStorage quota exhausted.

Phase 3 — mobile gestures (runs only on chromium-mobile / Pixel 7):
  09-mobile/    touch-target ≥44px audit, env(safe-area-inset-bottom)
                structural check, long-press (FeedListCard → ContextSheet,
                quick-tap negation, click-suppression), double-tap
                (feed card like + lightbox heart-burst, via synthetic
                pointer events to bypass the first-tap-fires-click trap),
                viewport reflow (portrait/landscape/narrow/phablet),
                plus fixme stubs documenting planned gestures (swipe
                lightbox L/R, swipe-down dismiss, pull-to-refresh,
                long-press-comment).

Cross-UA matrix (chromium-engine projects run @smoke only):
  chromium-pixel7, chromium-galaxy-s22, samsung-internet (Samsung UA
  emulation on Galaxy viewport), edge-android, plus webkit-iphone,
  chrome-ios, firefox-android, firefox-desktop — the latter four need
  libavif16 on the host (Playwright dep) but the configs are in place.

Infrastructure:
  - fixtures/test.ts central test.extend (api, db, adminToken, guest,
    host, signIn). Per-test DB truncate via the dev-only POST
    /admin/__truncate route, gated by EVENTSNAP_TEST_MODE=1.
  - helpers/sse-listener.ts, helpers/upload-client.ts (Node-side
    multipart for adversarial file-upload tests + JPEG/PNG/ELF magic
    constants), helpers/touch.ts (longPress / doubleTap / swipe /
    inlineStyle / computedStyle).
  - 10 page objects covering every route + UploadSheet/Lightbox.
  - global-setup waits for /health, logs in admin, disables every
    rate-limit and quota toggle.
  - .github/workflows/e2e.yml: PR check runs chromium-desktop + the
    smoke matrix in parallel, uploads playwright-report/ and traces on
    failure.

Findings the suite surfaces as live `[finding]` warnings (not silenced):
  1. /admin/login has no rate-limit or lockout (bcrypt cost only).
  2. PIN-attempt counter races under parallel /recover requests.
  3. Zero-byte uploads pass /api/v1/upload.
  4. SVG-with-script can pass the magic-byte check (consider CSP +
     X-Content-Type-Options on /media/*).

Stack-internal docs live in e2e/README.md (UA tier table, Samsung
Internet escalation tiers A/B/C, debugging tips, roadmap).

Final tally: 134 passed / 0 failed / 9 skipped (test.fixme stubs for
not-yet-shipped gestures and one UI-upload-flow investigation).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 19:37:11 +02:00

61 lines
2.5 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Phase 3 mobile — viewport reflow.
*
* Asserts the layout still works at landscape orientation, a narrow
* "small phone" viewport, and a "phablet" viewport. The bottom nav must
* remain reachable; the FAB stays centered; no horizontal overflow.
*/
import { test, expect } from '../../fixtures/test';
const VIEWPORTS = [
{ name: 'portrait (default Pixel 7)', width: 412, height: 915 },
{ name: 'landscape (Pixel 7 rotated)', width: 915, height: 412 },
{ name: 'narrow small phone', width: 320, height: 568 },
{ name: 'phablet', width: 480, height: 1024 },
];
test.describe('Mobile — viewport reflow', () => {
for (const vp of VIEWPORTS) {
test(`bottom nav remains usable at ${vp.name} (${vp.width}×${vp.height})`, async ({ page, guest, signIn }) => {
const g = await guest(`Reflow_${vp.width}x${vp.height}`);
await signIn(page, g);
await page.setViewportSize({ width: vp.width, height: vp.height });
await page.goto('/feed');
const nav = page.locator('nav').filter({ has: page.getByRole('link', { name: 'Galerie' }) }).first();
const fab = page.getByRole('button', { name: 'Hochladen' });
await expect(nav).toBeVisible();
await expect(fab).toBeVisible();
// No horizontal overflow on <html>.
const overflowX = await page.evaluate(() => {
const html = document.documentElement;
return html.scrollWidth - html.clientWidth;
});
expect.soft(overflowX, 'no horizontal overflow').toBeLessThanOrEqual(1);
// FAB is roughly centered: its x-mid should be within 30% of the viewport mid.
const fabBox = await fab.boundingBox();
if (!fabBox) throw new Error('FAB has no bounding box');
const fabMidX = fabBox.x + fabBox.width / 2;
const expectedMid = vp.width / 2;
expect.soft(Math.abs(fabMidX - expectedMid)).toBeLessThanOrEqual(vp.width * 0.30);
});
}
test('rotation portrait → landscape preserves auth + bottom nav', async ({ page, guest, signIn }) => {
const g = await guest('Rotate');
await signIn(page, g);
await page.goto('/feed');
await expect(page.getByRole('link', { name: 'Galerie' })).toBeVisible();
await page.setViewportSize({ width: 915, height: 412 });
// The same nav should still be visible — no layout shift forces a re-render that loses auth.
await expect(page.getByRole('link', { name: 'Galerie' })).toBeVisible();
const stillAuthed = await page.evaluate(() => !!localStorage.getItem('eventsnap_jwt'));
expect(stillAuthed).toBe(true);
});
});