diff --git a/.gitignore b/.gitignore index 298d097..103fd45 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,6 @@ infra/volumes/ apps/web/playwright-report/ apps/web/test-results/ apps/web/tests/.auth/ + +# Claude Code per-project runtime state (schedulers etc.) +.claude/ diff --git a/apps/web/.env.development b/apps/web/.env.development index af75edb..96e7608 100644 --- a/apps/web/.env.development +++ b/apps/web/.env.development @@ -5,9 +5,9 @@ # that Kong + GoTrue + PostgREST use (see root .env). The repo-wide .env is the # authoritative source; this file exists so non-sensitive PUBLIC_* values can be # versioned for new contributors. If you rotate JWT_SECRET, update this file too. -PUBLIC_SUPABASE_URL=http://localhost:8001 +PUBLIC_SUPABASE_URL=http://192.168.1.167:8001 PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0 -PUBLIC_KEYCLOAK_URL=http://keycloak:8080 +PUBLIC_KEYCLOAK_URL=http://192.168.1.167:8080 PUBLIC_KEYCLOAK_REALM=colectivo PUBLIC_KEYCLOAK_CLIENT_ID=colectivo-web -PUBLIC_APP_URL=http://localhost:5173 +PUBLIC_APP_URL=http://192.168.1.167:5173 diff --git a/apps/web/playwright.config.ts b/apps/web/playwright.config.ts index 3c75e4e..33372eb 100644 --- a/apps/web/playwright.config.ts +++ b/apps/web/playwright.config.ts @@ -14,7 +14,10 @@ export default defineConfig({ reporter: [['html', { open: 'never' }], ['list']], use: { - baseURL: 'http://localhost:5173', + // Match PUBLIC_APP_URL so the OAuth round-trip (Keycloak → GoTrue → SPA) + // lands on the same origin the test started from. When PUBLIC_APP_URL is + // a LAN IP (for phone/tablet previews) tests continue to work. + baseURL: process.env.PUBLIC_APP_URL ?? 'http://localhost:5173', trace: 'on-first-retry', screenshot: 'only-on-failure', // Load anon storage state by default; individual tests override as needed diff --git a/apps/web/tests/e2e/items.test.ts b/apps/web/tests/e2e/items.test.ts index 22c2eda..8f55fa2 100644 --- a/apps/web/tests/e2e/items.test.ts +++ b/apps/web/tests/e2e/items.test.ts @@ -85,7 +85,7 @@ test.describe('Shopping items — member (Borja)', () => { const itemName = `Delete-me-${Date.now()}`; await nameInput.fill(itemName); await nameInput.press('Enter'); - await expect(page.getByText(itemName)).toBeVisible({ timeout: 5_000 }); + await expect(itemRow(page, itemName)).toBeVisible({ timeout: 5_000 }); const row = itemRow(page, itemName); await row.hover(); @@ -95,7 +95,11 @@ test.describe('Shopping items — member (Borja)', () => { // button is the one with `sm:flex` — target it via class. await row.locator('button.sm\\:flex[aria-label="Delete"]').click(); - await expect(page.getByText(itemName)).not.toBeVisible({ timeout: 5_000 }); + // After Fase 5.5 the delete goes through the undo queue: the row is + // removed immediately, but a toast briefly shows "Deleted " — that + // substring matches `getByText(itemName)`. Scope the assertion to the + // item list (role="listitem") so the toast is ignored. + await expect(itemRow(page, itemName)).not.toBeVisible({ timeout: 5_000 }); }); test('D-06: frequency suggestions render below the add-item input', async ({ page }) => { diff --git a/apps/web/tests/fixtures/login.ts b/apps/web/tests/fixtures/login.ts index 64a3029..c971185 100644 --- a/apps/web/tests/fixtures/login.ts +++ b/apps/web/tests/fixtures/login.ts @@ -18,12 +18,12 @@ export async function loginAs(page: Page, user: (typeof USERS)[keyof typeof USER await page.fill('#password', user.password); await page.click('#kc-login'); - // Wait until we're redirected OFF the callback page. The root layout's - // post-login routing sends the user to /lists (or /onboarding) only after - // the deferred loadUserCollectives call resolves, so we can't call the - // login done until we've left /auth/callback. + // Wait until we're redirected OFF the callback page. Origin depends on the + // PUBLIC_APP_URL the stack is configured with (localhost for plain dev; LAN + // IP like http://192.168.1.167:5173 when phones are in the loop). + const appOrigin = new URL(process.env.PUBLIC_APP_URL ?? 'http://localhost:5173').origin; await page.waitForURL( - (url) => url.origin === 'http://localhost:5173' && !url.pathname.startsWith('/auth/callback'), + (url) => url.origin === appOrigin && !url.pathname.startsWith('/auth/callback'), { timeout: 20_000 } ); await page.waitForFunction( @@ -34,4 +34,27 @@ export async function loginAs(page: Page, user: (typeof USERS)[keyof typeof USER 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 } + ); } diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index 19a1e23..2ec3f34 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -46,6 +46,12 @@ export default defineConfig({ }) ], server: { - port: 5173 + port: 5173, + // Bind to all interfaces so the LAN (phone, tablet, other laptops on + // the same Wi-Fi) can reach the dev server at http://:5173. + // Auth flows will fail from non-laptop clients because Keycloak's + // issuer URL resolves to `keycloak:8080` only on the laptop's /etc/hosts + // — use this for layout/CSS visual checks, not for full login tests. + host: true } }); diff --git a/infra/docker-compose.dev.yml b/infra/docker-compose.dev.yml index bab2a9f..7305e95 100644 --- a/infra/docker-compose.dev.yml +++ b/infra/docker-compose.dev.yml @@ -45,6 +45,13 @@ services: GOTRUE_DB_DRIVER: postgres GOTRUE_DB_DATABASE_URL: postgres://supabase_auth_admin:${POSTGRES_PASSWORD:-postgres}@db:5432/postgres GOTRUE_SITE_URL: ${PUBLIC_APP_URL:-http://localhost:5173} + # Allow the SPA's /auth/callback subpath (where the /auth/callback +page + # calls exchangeCodeForSession). Without this GoTrue rejects the + # redirect_to query param and falls back to SITE_URL, stranding `?code=` + # at `/` instead of `/auth/callback` — which then re-triggers signIn + # and creates an infinite login loop. We also allow localhost so that + # `just test-all` runs can still hit the app through the loopback. + GOTRUE_URI_ALLOW_LIST: "http://192.168.1.167:5173/auth/callback,http://localhost:5173/auth/callback,http://localhost:3000/auth/callback" GOTRUE_JWT_SECRET: ${SUPABASE_JWT_SECRET} GOTRUE_JWT_EXP: 3600 # Allow OIDC-based user creation (Keycloak is the gatekeeper). @@ -58,11 +65,15 @@ services: GOTRUE_EXTERNAL_KEYCLOAK_SECRET: ${KEYCLOAK_CLIENT_SECRET:-gotrue-dev-secret} # openid is required so the access token is an OIDC token and the userinfo endpoint accepts it. GOTRUE_EXTERNAL_KEYCLOAK_SCOPES: "openid profile email" - # GoTrue fetches Keycloak's OIDC discovery doc at this URL (Docker-internal hostname). - # The browser also uses this hostname — add "127.0.0.1 keycloak" to /etc/hosts. - GOTRUE_EXTERNAL_KEYCLOAK_URL: http://keycloak:8080/realms/colectivo - # Keycloak redirects the browser here after login; GoTrue exchanges the code. - GOTRUE_EXTERNAL_KEYCLOAK_REDIRECT_URI: http://localhost:8001/auth/v1/callback + # Must be reachable from the browser (for the 302 to Keycloak's /auth + # endpoint) AND from inside this container (for the server-side code→token + # exchange). The LAN IP in PUBLIC_KEYCLOAK_URL satisfies both: Docker + # bridge routes container→host via the default gateway, then Docker's + # port-forward loops 8080 back to the Keycloak container. + GOTRUE_EXTERNAL_KEYCLOAK_URL: ${PUBLIC_KEYCLOAK_URL:-http://keycloak:8080}/realms/colectivo + # Browser-reachable URL that Keycloak 302s back to after login. Must be + # listed in the colectivo-web client's redirectUris in the realm. + GOTRUE_EXTERNAL_KEYCLOAK_REDIRECT_URI: ${PUBLIC_SUPABASE_URL:-http://localhost:8001}/auth/v1/callback GOTRUE_MAILER_AUTOCONFIRM: "true" # ── PostgREST ─────────────────────────────────────────────────────────────── diff --git a/keycloak/realm-export.json b/keycloak/realm-export.json index 501cb9e..d74b8b0 100644 --- a/keycloak/realm-export.json +++ b/keycloak/realm-export.json @@ -300,11 +300,15 @@ "clientAuthenticatorType": "client-secret", "secret": "gotrue-dev-secret", "redirectUris": [ + "http://192.168.1.167:5173/*", + "http://192.168.1.167:8001/auth/v1/callback", "http://localhost:3000/*", "http://localhost:5173/*", "http://localhost:8001/auth/v1/callback" ], "webOrigins": [ + "http://192.168.1.167:5173", + "http://192.168.1.167:8001", "http://localhost:3000", "http://localhost:5173", "http://localhost:8001" @@ -983,7 +987,7 @@ { "id": "a09a9441-c626-4a26-8723-475d2d4b2356", "name": "openid", - "description": "OpenID Connect scope — ensures access token includes openid scope claim for userinfo endpoint", + "description": "OpenID Connect scope \u2014 ensures access token includes openid scope claim for userinfo endpoint", "protocol": "openid-connect", "attributes": { "include.in.token.scope": "true",