fix(sprint-6/etapa-3): localStorage key en ProfileSheet + eliminar setTimeout(350) + specs E2E de auth
Some checks failed
/ Deploy to Cloudflare Pages (push) Has been cancelled
Some checks failed
/ Deploy to Cloudflare Pages (push) Has been cancelled
This commit is contained in:
72
tests/e2e/auth-localStorage-key.spec.ts
Normal file
72
tests/e2e/auth-localStorage-key.spec.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Diagnóstico Sprint 6 Etapa 3 Parte A: ¿bajo qué clave de localStorage escribe
|
||||
* supabase-js v2 la sesión en este entorno? ProfileSheet.getAccessToken() lee
|
||||
* 'supabase.auth.token' (STORAGE_KEY default de @supabase/auth-js GoTrueClient),
|
||||
* pero auth-setup.ts documenta que @supabase/supabase-js's createClient() sobrescribe
|
||||
* ese default calculando `sb-${projectRef}-auth-token` desde la URL del proyecto —
|
||||
* a menos que el caller pase un storageKey explícito (src/lib/supabase.ts no lo hace).
|
||||
*
|
||||
* Este spec confirma empíricamente cuál de las dos claves existe realmente en el
|
||||
* localStorage de una sesión autenticada real, antes de tocar ProfileSheet.tsx.
|
||||
*/
|
||||
import { test, expect } from '@playwright/test'
|
||||
import { readFileSync, existsSync } from 'fs'
|
||||
import { resolve } from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
test.use({ storageState: 'tests/e2e/setup/auth-state.json' })
|
||||
|
||||
const __dirname = fileURLToPath(new URL('.', import.meta.url))
|
||||
const ROOT = resolve(__dirname, '../..')
|
||||
|
||||
// Mismo patrón manual que auth-setup.ts / export-pdf.spec.ts — no hay dotenv
|
||||
// cargando process.env automáticamente en el runner de Playwright.
|
||||
function loadEnvFile(filename: string): Record<string, string> {
|
||||
const path = resolve(ROOT, filename)
|
||||
if (!existsSync(path)) return {}
|
||||
const out: Record<string, string> = {}
|
||||
for (const line of readFileSync(path, 'utf-8').split('\n')) {
|
||||
const trimmed = line.trim()
|
||||
if (!trimmed || trimmed.startsWith('#')) continue
|
||||
const idx = trimmed.indexOf('=')
|
||||
if (idx === -1) continue
|
||||
out[trimmed.slice(0, idx)] = trimmed.slice(idx + 1)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
test('diagnóstico: confirmar la clave real de localStorage para la sesión Supabase', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
// Esperar a que la app cargue (AuthContext lee el storageState y monta el header)
|
||||
await page.waitForSelector('header button[title]', { timeout: 10_000 })
|
||||
|
||||
const localEnv = loadEnvFile('.env.local')
|
||||
const supabaseUrl = localEnv.VITE_SUPABASE_URL
|
||||
const projectRef = supabaseUrl ? new URL(supabaseUrl).hostname.split('.')[0] : null
|
||||
|
||||
const diagnosis = await page.evaluate((ref: string | null) => {
|
||||
const keys = Object.keys(localStorage)
|
||||
const authKeys = keys.filter((k) => k.includes('auth') || k.startsWith('sb-'))
|
||||
|
||||
const legacyRaw = localStorage.getItem('supabase.auth.token')
|
||||
const v2Key = ref ? `sb-${ref}-auth-token` : null
|
||||
const v2Raw = v2Key ? localStorage.getItem(v2Key) : null
|
||||
|
||||
return {
|
||||
allAuthRelatedKeys: authKeys,
|
||||
legacyKeyExists: legacyRaw !== null,
|
||||
v2KeyName: v2Key,
|
||||
v2KeyExists: v2Raw !== null,
|
||||
legacyHasAccessToken: legacyRaw ? Boolean((JSON.parse(legacyRaw) as { access_token?: string }).access_token) : false,
|
||||
v2HasAccessToken: v2Raw ? Boolean((JSON.parse(v2Raw) as { access_token?: string }).access_token) : false,
|
||||
}
|
||||
}, projectRef)
|
||||
|
||||
console.log('[diagnóstico localStorage]', JSON.stringify(diagnosis, null, 2))
|
||||
|
||||
// Afirmación central: la clave v2 (sb-{projectRef}-auth-token) es la que existe y
|
||||
// tiene un access_token real. La clave legacy ('supabase.auth.token') NO existe.
|
||||
expect(diagnosis.v2KeyExists, `Esperaba que ${diagnosis.v2KeyName} exista en localStorage`).toBe(true)
|
||||
expect(diagnosis.v2HasAccessToken, 'La clave v2 debe contener un access_token no vacío').toBe(true)
|
||||
expect(diagnosis.legacyKeyExists, "'supabase.auth.token' (clave legacy que lee ProfileSheet hoy) NO debería existir").toBe(false)
|
||||
})
|
||||
56
tests/e2e/auth-real.spec.ts
Normal file
56
tests/e2e/auth-real.spec.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Sprint 6 Etapa 3 Parte C — Validación de que el auth E2E funciona realmente
|
||||
* (no solo que el storageState se carga, sino que Supabase devuelve datos reales
|
||||
* del usuario autenticado).
|
||||
*
|
||||
* No existe data-testid="app-header-user-avatar" en AppHeader.tsx — el trigger del
|
||||
* dropdown es un <button title={user.name}> dentro del <header> (mismo selector que
|
||||
* ya usa tests/e2e/sprint-4-smoke.spec.ts: 'header button[title]').
|
||||
*/
|
||||
import { test, expect } from '@playwright/test'
|
||||
import { readFileSync, existsSync } from 'fs'
|
||||
import { resolve } from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
test.use({ storageState: 'tests/e2e/setup/auth-state.json' })
|
||||
|
||||
const __dirname = fileURLToPath(new URL('.', import.meta.url))
|
||||
const ROOT = resolve(__dirname, '../..')
|
||||
|
||||
function loadEnvFile(filename: string): Record<string, string> {
|
||||
const path = resolve(ROOT, filename)
|
||||
if (!existsSync(path)) return {}
|
||||
const out: Record<string, string> = {}
|
||||
for (const line of readFileSync(path, 'utf-8').split('\n')) {
|
||||
const trimmed = line.trim()
|
||||
if (!trimmed || trimmed.startsWith('#')) continue
|
||||
const idx = trimmed.indexOf('=')
|
||||
if (idx === -1) continue
|
||||
out[trimmed.slice(0, idx)] = trimmed.slice(idx + 1)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
const testEnv = loadEnvFile('.env.test')
|
||||
const TEST_USER_EMAIL = testEnv.TEST_USER_EMAIL
|
||||
|
||||
test('auth E2E: Supabase devuelve datos reales del usuario autenticado', async ({ page }) => {
|
||||
test.skip(!TEST_USER_EMAIL, 'TEST_USER_EMAIL no configurada en .env.test — ver .env.example')
|
||||
|
||||
await page.goto('/')
|
||||
|
||||
// Esperar a que AppHeader muestre el trigger del dropdown de usuario
|
||||
// (confirma que AuthContext cargó la sesión y el avatar renderiza con user.name real)
|
||||
const avatarTrigger = page.locator('header button[title]').first()
|
||||
await expect(avatarTrigger).toBeVisible({ timeout: 10_000 })
|
||||
|
||||
// El título del botón es el nombre real del usuario — no debe estar vacío
|
||||
const titleAttr = await avatarTrigger.getAttribute('title')
|
||||
expect(titleAttr, 'el trigger debe tener el nombre real del usuario, no vacío').toBeTruthy()
|
||||
|
||||
// Abrir el dropdown y confirmar que el email del usuario de test aparece —
|
||||
// esto requiere que AuthContext haya construido el AuthUser real desde la sesión
|
||||
// de Supabase (no un mock ni un valor placeholder).
|
||||
await avatarTrigger.click()
|
||||
await expect(page.getByText(TEST_USER_EMAIL)).toBeVisible({ timeout: 5_000 })
|
||||
})
|
||||
Reference in New Issue
Block a user