Files
EventSnap/e2e/specs/01-auth/join.spec.ts
fabi bbdfae09a0 chore(e2e): add ESLint + Prettier; fix real findings; dedupe BASE
The Playwright suite had no linter and no formatter — only tsc. Add flat-config ESLint
(typescript-eslint, type-aware) and Prettier (2-space, matching the suite's style).

Rules keep the ones that catch real TEST bugs and drop the noise:
  - no-floating-promises KEPT — an un-awaited request/assertion can let a test end before it runs,
    passing vacuously. It caught one: the SSE reader loop in sse-listener is now explicitly `void`.
  - no-unused-vars KEPT — caught three dead bindings (an unused adminToken fixture arg, an unused
    `api` arg, an unused JPEG_MAGIC import), all removed.
  - no-explicit-any OFF — all test code; `any` is the honest type for an untyped res.json() body or
    a page.evaluate() return.
  - no-empty-pattern OFF — Playwright's dependency-free fixtures are `async ({}, use) => {}`.

Refactor: `const BASE = process.env.E2E_FRONTEND_URL ?? '...'` was redeclared verbatim in 23
files — extracted to helpers/env.ts and imported, so a port/scheme change is one edit not a sweep.

Then `prettier --write`. Verified: eslint clean, tsc clean, prettier clean, desktop suite 210
passed / 1 skipped. (One mobile spec flaked once under retries:0 — a pre-existing cross-test
reflow-timing vector from the flakiness audit, not this change: the each-key edit is stable across
16 isolated runs and a clean full mobile re-run.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 20:45:59 +02:00

143 lines
5.3 KiB
TypeScript

/**
* USER_JOURNEYS.md §1 (First-time guest), §2 (Returning guest, same device),
* §3 (Returning guest, new device). Covers the happy path through
* /join, the PIN modal, the onboarding overlay landing, and the
* name-already-taken recovery transformation.
*/
import { test, expect } from '../../fixtures/test';
import { JoinPage } from '../../page-objects';
import { readStorage, STORAGE_KEYS, clearAllStorage } from '../../helpers/storage-helpers';
test.describe('Auth — join flow', () => {
test('happy path: name → PIN modal → feed @smoke', async ({ page }) => {
const join = new JoinPage(page);
await join.goto();
await expect(page.getByRole('heading', { name: 'Willkommen!' })).toBeVisible();
const { pin } = await join.joinAs('Alice');
expect(pin).toMatch(/^\d{4}$/);
// PIN copy button toggles to "Kopiert!" on click
await join.pinCopyButton.click();
await expect(join.pinCopyButton).toHaveText(/Kopiert/i);
await join.continueToFeed();
await expect(page).toHaveURL(/\/feed$/);
const storage = await readStorage(page);
expect(storage.jwt, 'JWT in localStorage').toMatch(/^eyJ/);
expect(storage.pin).toBe(pin);
expect(storage.userId).toMatch(/^[0-9a-f-]{36}$/);
expect(storage.displayName).toBe('Alice');
});
test('returning guest with valid JWT is redirected to /feed', async ({ page, guest, signIn }) => {
const alice = await guest('Bob');
await signIn(page, alice);
// Now visit the root — should auto-redirect to /feed.
await page.goto('/');
await page.waitForURL('**/feed', { timeout: 5_000 });
});
test('returning guest, new device: same name shows the inline recovery form', async ({
page,
guest,
}) => {
const original = await guest('Charlie');
// Brand-new browser context (cleared storage) — landing on /join with same name
await clearAllStorage(page);
const join = new JoinPage(page);
await join.goto();
await join.fillName('Charlie');
await join.submit();
await expect(join.recoveryPinInput).toBeVisible();
await expect(page.getByText(/Charlie.*bereits vergeben/)).toBeVisible();
// Type correct PIN → land on /feed with a new JWT. Filling the 4th digit
// auto-submits (see pin-auto-submit.spec), so an explicit submit click would
// race the navigation; click only as a fallback if the button is still around.
await join.recoveryPinInput.fill(original.pin);
if (await join.recoverySubmit.isEnabled().catch(() => false)) {
await join.recoverySubmit.click().catch(() => {});
}
await page.waitForURL('**/feed');
const storage = await readStorage(page);
expect(storage.userId).toBe(original.userId);
expect(storage.pin).toBe(original.pin);
});
test('wrong PIN three times locks the account for 15 minutes', async ({ page, guest, db }) => {
const dave = await guest('Dave');
await clearAllStorage(page);
const join = new JoinPage(page);
await join.goto();
await join.fillName('Dave');
await join.submit();
await expect(join.recoveryPinInput).toBeVisible();
// Wrong PIN (real one is dave.pin)
const wrong = dave.pin === '0000' ? '1111' : '0000';
for (let i = 0; i < 3; i++) {
await join.recoveryPinInput.fill(wrong);
await join.recoverySubmit.click();
await expect(join.recoveryError).toBeVisible();
}
// Fourth attempt should hit the 429 lockout (even with the correct PIN now)
await join.recoveryPinInput.fill(dave.pin);
await join.recoverySubmit.click();
await expect(join.recoveryError).toContainText(/15 Minuten/);
// Sanity: DB row reflects the lock
// (The handler sets pin_locked_until directly — verify via API "recover" returning 429)
void db; // unused for now, documenting that db.lockUserPin exists if we want shortcut path
});
test('"Anderen Namen wählen" returns to the normal join form', async ({ page, guest }) => {
await guest('Eve');
await clearAllStorage(page);
const join = new JoinPage(page);
await join.goto();
await join.fillName('Eve');
await join.submit();
await expect(join.recoveryPinInput).toBeVisible();
await join.tryDifferentNameButton.click();
await expect(join.nameInput).toBeVisible();
await expect(join.recoveryPinInput).not.toBeVisible();
});
test('"Ich habe bereits einen Account" link routes to /recover', async ({ page }) => {
const join = new JoinPage(page);
await join.goto();
await join.linkToRecover.click();
await expect(page).toHaveURL(/\/recover$/);
});
test('JWT and PIN keys are exactly the ones auth.ts expects', async ({ page, guest, signIn }) => {
const handle = await guest('Frank');
await signIn(page, handle);
// Read raw localStorage to make sure no test accidentally uses a different key.
const raw = await page.evaluate(() => ({
jwt: localStorage.getItem('eventsnap_jwt'),
pin: localStorage.getItem('eventsnap_pin'),
userId: localStorage.getItem('eventsnap_user_id'),
displayName: localStorage.getItem('eventsnap_display_name'),
}));
expect(raw.jwt).toBe(handle.jwt);
expect(raw.pin).toBe(handle.pin);
expect(raw.userId).toBe(handle.userId);
expect(raw.displayName).toBe('Frank');
// Sanity: the keys we just checked match STORAGE_KEYS in storage-helpers.ts
expect(STORAGE_KEYS.jwt).toBe('eventsnap_jwt');
});
});