/** * 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], opts: { waitForCollective?: boolean } = {} ): Promise { const { waitForCollective = true } = opts; 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 } ); // Opt-out for no-collective users (Eva): the sidebar never appears because // the root layout sends them straight to /onboarding, so the emoji-button // heuristic below would time out with no useful signal. Callers that exercise // the onboarding flow pass `waitForCollective: false`. if (!waitForCollective) return; // 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 } ); }