Root cause of the recurring "just test-all shows 27/16, playwright alone shows 43/0" split: Justfile loads .env with dotenv-load, which sets PUBLIC_APP_URL to the LAN IP from the phone-preview config. playwright.config.ts was reading that env and using it as baseURL. The resulting LAN-IP OAuth round-trip (browser → GoTrue → Keycloak → browser) adds ~hundreds of ms of loopback routing per hop and pushes the $currentCollective hydration past Playwright's first synthetic keystroke, producing a silent early return in handleAdd (name typed but Enter never triggers a write). Running playwright directly skipped dotenv-load, baseURL fell back to localhost, and everything passed. Same code, same machine, different wrapper. Fix apps/web/playwright.config.ts: baseURL hardcoded to 'http://localhost:5173'. The LAN IP stays in PUBLIC_APP_URL for on-device phone previews (step 2/3 of the mobile-testing recipe in CLAUDE.md), but e2e always hits the loopback so the timing profile is deterministic. apps/web/tests/fixtures/login.ts: origin-check hardcoded to localhost to match. Verification just test-all → exit 0, 228 tests green (34 pgTAP + 140 Vitest integration + 11 Vitest unit + 43 Playwright), 4 skipped (upstream Realtime presence + WebKit-only touch gestures). just test-e2e alone → 43 passed. pnpm exec playwright test alone → 43 passed. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
60 lines
2.3 KiB
TypeScript
60 lines
2.3 KiB
TypeScript
/**
|
||
* Live login helper — runs the real Keycloak OAuth flow in the current context.
|
||
*
|
||
* We use this instead of `storageState` because Supabase JS v2's async
|
||
* `_recoverAndRefresh` doesn't reliably re-fire `onAuthStateChange`/INITIAL_SESSION
|
||
* when the browser context is built from a saved localStorage snapshot. Doing the
|
||
* full login for each test adds ~2–3 seconds but produces deterministic state.
|
||
*/
|
||
import type { Page } from '@playwright/test';
|
||
import type { USERS } from './users.js';
|
||
|
||
export async function loginAs(page: Page, user: (typeof USERS)[keyof typeof USERS]): Promise<void> {
|
||
await page.goto('/');
|
||
await page.waitForURL(/\/realms\/colectivo\/protocol\/openid-connect\/auth/, {
|
||
timeout: 15_000
|
||
});
|
||
await page.fill('#username', user.email);
|
||
await page.fill('#password', user.password);
|
||
await page.click('#kc-login');
|
||
|
||
// We pin Playwright's baseURL to http://localhost:5173 (see playwright.config.ts)
|
||
// regardless of whether PUBLIC_APP_URL is the LAN IP for device previews, so
|
||
// the post-login origin is always localhost.
|
||
await page.waitForURL(
|
||
(url) => url.origin === 'http://localhost:5173' && !url.pathname.startsWith('/auth/callback'),
|
||
{ timeout: 20_000 }
|
||
);
|
||
await page.waitForFunction(
|
||
() =>
|
||
Object.keys(window.localStorage).some(
|
||
(k) => k.startsWith('sb-') && k.endsWith('-auth-token')
|
||
),
|
||
null,
|
||
{ timeout: 15_000 }
|
||
);
|
||
|
||
// Wait for `$currentCollective` to populate — any test that creates rows
|
||
// needs this, because `handleCreate` silently returns when collective is
|
||
// null. On localhost the round-trip was fast enough that races were rare,
|
||
// but over LAN IP the extra latency exposes the race. We detect it by the
|
||
// sidebar's collective button rendering the actual name (otherwise it
|
||
// shows the app_name fallback "Colectivo").
|
||
await page.waitForFunction(
|
||
(appName) => {
|
||
const buttons = document.querySelectorAll('button');
|
||
for (const b of Array.from(buttons)) {
|
||
const txt = b.textContent?.trim();
|
||
if (txt && txt !== appName && !txt.startsWith(appName)) {
|
||
// Heuristic: any non-default button with emoji-prefixed name
|
||
// (seed collective is "Casa García-López" with emoji "🏠").
|
||
if (/\p{Emoji}/u.test(txt)) return true;
|
||
}
|
||
}
|
||
return false;
|
||
},
|
||
'Colectivo',
|
||
{ timeout: 10_000 }
|
||
);
|
||
}
|