La política simulations_update está activa en la DB de desarrollo. El fix B está completo.
Some checks failed
/ Deploy to Cloudflare Pages (push) Has been cancelled
Some checks failed
/ Deploy to Cloudflare Pages (push) Has been cancelled
Resumen de todo lo entregado en Etapa 7: Fix A1 (src/auth/AuthContext.tsx): isProfileLoaded: boolean agregado como estado. Arranca en false, se pone true en el finally del sync effect (después de syncProfileFromDB), y cuando onAuthStateChange devuelve null (usuario no autenticado). Se resetea a false en cada SIGN_IN para gatear el re-sync. Fix A2 (src/features/library/LibraryPage.tsx): Guard temprano después de todos los hooks — si !user || !isProfileLoaded devuelve un skeleton de tres columnas animado, idéntico al que usa HomeView. El contenido rol-dependiente solo se renderiza cuando el perfil está sincronizado. Fix A3 (GlobalSettingsPanel.tsx + WorkspacePage.tsx): El campo "Cliente" es texto estático (<p>) para client_editor y client_viewer. En el topbar de WorkspacePage se oculta el input inline y el ícono de lápiz; si hay un clientName se muestra como texto inerte. Fix B (supabase/migrations/016_add_simulations_update_policy.sql): Migración creada + aplicada vía Management API a inq-roi-desa. pg_policies confirma las 4 políticas: simulations_select, simulations_insert, simulations_update, simulations_delete. Tests: tests/features/library/library-ui.test.tsx actualizado — mock incluye isProfileLoaded, 2 tests nuevos para isProfileLoaded: false. Total: 586 tests, todos verdes. Build limpio.
This commit is contained in:
176
sprints/sprint-7/ETAPA_7_PROMPT.md
Normal file
176
sprints/sprint-7/ETAPA_7_PROMPT.md
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
# Etapa 7 — Fix: race condition de roles en LibraryPage + simulations UPDATE policy
|
||||||
|
|
||||||
|
**Sprint:** 7
|
||||||
|
**Fecha:** 2026-07-07
|
||||||
|
**Alcance:** AuthContext (loading state), LibraryPage (guard), nueva migración SQL (`simulations_update`), campo client read-only para client_editor.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Contexto del bug
|
||||||
|
|
||||||
|
`msebem@gmail.com` tiene `platform_role = 'client_editor'` confirmado en DB.
|
||||||
|
Sin embargo al cargar LibraryPage muestra elementos que solo debería ver InQuality:
|
||||||
|
- Botón "Importar BPMN"
|
||||||
|
- Panel de grupos ("Clientes")
|
||||||
|
- Ícono de configuración
|
||||||
|
|
||||||
|
Al navegar a otra ruta y volver, el comportamiento es correcto (elementos ocultos).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Causa raíz confirmada — DOS problemas independientes
|
||||||
|
|
||||||
|
### Problema A: race condition de roles en LibraryPage
|
||||||
|
|
||||||
|
**Flujo actual:**
|
||||||
|
1. Login → `onAuthStateChange` SIGNED_IN → `buildAuthUser(supabaseUser)` → `platformRole: 'guest'` (heurística email; `msebem@gmail.com` no es `@inquality.com.py`)
|
||||||
|
2. `guest` → `isClientRole = false` → LibraryPage renderiza con todos los elementos visibles
|
||||||
|
3. `syncProfileFromDB` completa → `platformRole: 'client_editor'` → store actualizado
|
||||||
|
4. Navegar y volver → re-render correcto
|
||||||
|
|
||||||
|
La UI "miente" durante la ventana entre login y sync de DB. El fix es no renderizar contenido rol-dependiente hasta que `syncProfileFromDB` haya completado al menos una vez en la sesión.
|
||||||
|
|
||||||
|
### Problema B: política `simulations_update` inexistente
|
||||||
|
|
||||||
|
En `migration 015_client_roles_and_rls.sql` existe `simulations_insert` pero NO existe `simulations_update`.
|
||||||
|
|
||||||
|
Cuando "Guardar configuración" intenta hacer UPDATE sobre una simulación ya existente → PostgreSQL no encuentra política → operación bloqueada silenciosamente. El usuario no recibe error explícito pero los cambios no se persisten.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cambios requeridos
|
||||||
|
|
||||||
|
**ANTES de empezar:** leer `src/auth/AuthContext.tsx` (o el archivo que gestiona `onAuthStateChange` y llama a `syncProfileFromDB`) y `src/features/library/LibraryPage.tsx` para entender el estado actual. Leer también `src/features/workspace/WorkspacePage.tsx` para detectar si tiene el mismo problema.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Fix A1: agregar `isProfileLoaded` al AuthContext
|
||||||
|
|
||||||
|
En el contexto de autenticación (el que expone `user`, maneja `onAuthStateChange`, llama a `syncProfileFromDB`):
|
||||||
|
|
||||||
|
Agregar un estado `isProfileLoaded: boolean` (inicialmente `false`).
|
||||||
|
|
||||||
|
Lógica:
|
||||||
|
- Se pone en `false` cuando empieza el proceso de login (SIGNED_IN event)
|
||||||
|
- Se pone en `true` después de que `syncProfileFromDB` completa (tanto si retorna datos como si retorna null)
|
||||||
|
- Cuando el evento es `SIGNED_OUT`, volver a `false`
|
||||||
|
- Cuando el token se refresca (`TOKEN_REFRESHED`) o el usuario ya estaba logueado al cargar la app, también completar el sync antes de poner `true`
|
||||||
|
|
||||||
|
Exponer `isProfileLoaded` en el contexto junto a `user`.
|
||||||
|
|
||||||
|
**Importante:** `isProfileLoaded: true` con `user: null` es un estado válido (usuario no logueado, pero ya se verificó). `isProfileLoaded: false` con `user: null` es "todavía verificando".
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Fix A2: guard en LibraryPage
|
||||||
|
|
||||||
|
**ANTES de empezar este fix:** leer el archivo completo de LibraryPage para entender dónde se usa `isClientRole` y qué elementos condicionan.
|
||||||
|
|
||||||
|
En LibraryPage, antes de renderizar contenido rol-dependiente (import button, groups panel, settings gear, y cualquier sección condicionada por `isClientRole`), verificar `isProfileLoaded`.
|
||||||
|
|
||||||
|
**Si `!isProfileLoaded`:** mostrar un estado de carga mínimo. No es necesario un skeleton complejo — basta con el mismo spinner o skeleton que ya usa LibraryPage en su estado de carga (`isLoading && groups.length === 0`). Reutilizar ese patrón si existe.
|
||||||
|
|
||||||
|
**Si `isProfileLoaded`:** renderizar normalmente, el `isClientRole` ahora tiene el valor correcto.
|
||||||
|
|
||||||
|
La idea es que el usuario vea un spinner breve (< 200ms típicamente) en lugar de una UI incompleta que luego cambia. Preferible UX.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Fix A3: campo "cliente" read-only para client_editor (en WorkspacePage / GlobalPanel)
|
||||||
|
|
||||||
|
**ANTES de empezar:** leer el componente que renderiza el campo "cliente" (texto libre) en el sidebar o panel global del workspace. Puede estar en `WorkspacePage.tsx`, `GlobalPanel.tsx`, o similar.
|
||||||
|
|
||||||
|
Para `isClientRole = true`: mostrar el nombre del cliente como texto estático (no como input editable). El campo sigue visible pero no editable. No es necesario guardarlo o enviar cambios al hacer click en él.
|
||||||
|
|
||||||
|
Para InQuality (`!isClientRole`): comportamiento actual sin cambios.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Fix B: nueva migración — `simulations_update` policy
|
||||||
|
|
||||||
|
Crear el archivo de migración con el número siguiente a 015 (verificar qué existe en `supabase/migrations/` y usar el siguiente).
|
||||||
|
|
||||||
|
Nombre: `016_add_simulations_update_policy.sql`
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Fix: faltaba política UPDATE en simulations.
|
||||||
|
-- simulations_insert existe en 015 pero simulations_update no fue creada.
|
||||||
|
-- Sin esta policy, client_editor no puede guardar configuración sobre simulaciones existentes.
|
||||||
|
|
||||||
|
CREATE POLICY "simulations_update" ON public.simulations
|
||||||
|
FOR UPDATE USING (
|
||||||
|
EXISTS (SELECT 1 FROM public.processes WHERE id = simulations.process_id)
|
||||||
|
AND public.current_platform_role() NOT IN ('client_viewer', 'guest')
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Verificar antes de crear:** que no exista ya una `simulations_update` policy en la DB (puede haberse creado manualmente). Si ya existe, no crearla (o usar `DROP POLICY IF EXISTS` + recrear para idempotencia).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Fix B2: verificar y corregir rename de proceso para client_editor
|
||||||
|
|
||||||
|
**ANTES de empezar:** en WorkspacePage (o donde se maneja el rename del proceso), encontrar la lógica que guarda el nombre cuando el usuario lo edita.
|
||||||
|
|
||||||
|
La política `update_processes` en migration 015 **ya permite** `client_editor AND org_id = current_org()`. Si el rename no funciona, el problema es frontend (el input no llama al save, o el save usa el método incorrecto).
|
||||||
|
|
||||||
|
Verificar:
|
||||||
|
1. ¿El input de nombre del proceso llama a la función de save en `onBlur` o `onEnter`?
|
||||||
|
2. ¿Esa función usa `supabase.from('processes').update()` con el id correcto?
|
||||||
|
3. Si ambos están bien pero sigue fallando → verificar en Supabase SQL Editor que `UPDATE public.processes SET name = 'test' WHERE id = '<process_id>'` funcione como el usuario client_editor (hint: no se puede hacer directamente en SQL Editor como el usuario — verificar la condición de la policy manualmente)
|
||||||
|
|
||||||
|
Si es un bug frontend (el save no se dispara), arreglarlo.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tests nuevos
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Mínimo requerido:
|
||||||
|
|
||||||
|
// 1. LibraryPage no renderiza elements rol-dependientes hasta isProfileLoaded = true
|
||||||
|
// (simular: isProfileLoaded: false → assert import button no visible, groups panel no visible)
|
||||||
|
|
||||||
|
// 2. LibraryPage renderiza correctamente para client_editor una vez isProfileLoaded = true
|
||||||
|
// (simular: isProfileLoaded: true, platformRole: 'client_editor' → assert import button no visible, groups panel no visible)
|
||||||
|
|
||||||
|
// 3. LibraryPage renderiza correctamente para platform_admin una vez isProfileLoaded = true
|
||||||
|
// (simular: isProfileLoaded: true, platformRole: 'platform_admin' → assert import button visible)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Criterios de validación
|
||||||
|
|
||||||
|
- [ ] Loguear como `msebem@gmail.com` → LibraryPage NO muestra Import BPMN ni groups panel en ningún momento (ni durante la carga inicial)
|
||||||
|
- [ ] "Guardar configuración" en workspace funciona y persiste los cambios (no silent fail)
|
||||||
|
- [ ] El campo "cliente" en el sidebar del workspace es read-only para `msebem@gmail.com`
|
||||||
|
- [ ] Un usuario InQuality (platform_admin/member) sigue viendo todos los elementos sin regresión
|
||||||
|
- [ ] `npm run test` → tests verdes (incluidos los 3 nuevos mínimos)
|
||||||
|
- [ ] `npm run build` limpio
|
||||||
|
- [ ] **Validación visual del director antes del OK formal**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## NO modificar
|
||||||
|
|
||||||
|
- Edge Functions (`supabase/functions/`)
|
||||||
|
- Migraciones 001-015 (no editar, solo crear 016 nueva)
|
||||||
|
- Panel de admin (`/admin/*`)
|
||||||
|
- Motor de simulación, dominio, ROI, PDF, CSV
|
||||||
|
- `ConfirmInvitePage` (Etapa 6)
|
||||||
|
- `ReportPage`
|
||||||
|
- Lógica de cálculo en `domain/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Flujo esperado post-fix
|
||||||
|
|
||||||
|
1. `msebem@gmail.com` loguea
|
||||||
|
2. LibraryPage muestra spinner breve (~200ms)
|
||||||
|
3. `syncProfileFromDB` completa → `client_editor`
|
||||||
|
4. LibraryPage renderiza: lista flat de procesos de Solar Banco, sin import button, sin groups panel, sin settings gear
|
||||||
|
5. Entra al workspace de su proceso
|
||||||
|
6. Edita parámetros de actividades y configuración global
|
||||||
|
7. Hace click en "Guardar configuración" → persiste correctamente
|
||||||
|
8. Puede ver reporte y exportar PDF
|
||||||
@@ -6,6 +6,7 @@ import type { AuthUser } from './types'
|
|||||||
interface AuthContextValue {
|
interface AuthContextValue {
|
||||||
user: AuthUser | null
|
user: AuthUser | null
|
||||||
loading: boolean
|
loading: boolean
|
||||||
|
isProfileLoaded: boolean
|
||||||
signIn: () => Promise<void>
|
signIn: () => Promise<void>
|
||||||
signInWithEmailPassword: (email: string, password: string) => Promise<{ error: string | null }>
|
signInWithEmailPassword: (email: string, password: string) => Promise<{ error: string | null }>
|
||||||
signOut: () => Promise<void>
|
signOut: () => Promise<void>
|
||||||
@@ -65,6 +66,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
// Si es un OAuth redirect, mantener loading=true hasta que Supabase procese el token.
|
// Si es un OAuth redirect, mantener loading=true hasta que Supabase procese el token.
|
||||||
const [loading, setLoading] = useState(isOAuthRedirect)
|
const [loading, setLoading] = useState(isOAuthRedirect)
|
||||||
const [pendingPasswordReset, setPendingPasswordReset] = useState(false)
|
const [pendingPasswordReset, setPendingPasswordReset] = useState(false)
|
||||||
|
const [isProfileLoaded, setIsProfileLoaded] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Verificar y sincronizar con Supabase en background.
|
// Verificar y sincronizar con Supabase en background.
|
||||||
@@ -73,6 +75,10 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
const unsubscribe = authService.onAuthStateChange((u) => {
|
const unsubscribe = authService.onAuthStateChange((u) => {
|
||||||
setUser(u)
|
setUser(u)
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
|
// Si no hay usuario, no hay nada que sincronizar → marcar como cargado.
|
||||||
|
// Si hay usuario, la sincronización con DB reseteará a false → true al completar.
|
||||||
|
if (!u) setIsProfileLoaded(true)
|
||||||
|
else setIsProfileLoaded(false)
|
||||||
})
|
})
|
||||||
return () => unsubscribe()
|
return () => unsubscribe()
|
||||||
}, [])
|
}, [])
|
||||||
@@ -126,6 +132,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
console.error('Error al sincronizar perfil:', err)
|
console.error('Error al sincronizar perfil:', err)
|
||||||
} finally {
|
} finally {
|
||||||
clearTimeout(timeoutId)
|
clearTimeout(timeoutId)
|
||||||
|
setIsProfileLoaded(true)
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
|
|
||||||
@@ -158,6 +165,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
<AuthContext.Provider value={{
|
<AuthContext.Provider value={{
|
||||||
user,
|
user,
|
||||||
loading,
|
loading,
|
||||||
|
isProfileLoaded,
|
||||||
signIn,
|
signIn,
|
||||||
signInWithEmailPassword,
|
signInWithEmailPassword,
|
||||||
signOut,
|
signOut,
|
||||||
|
|||||||
@@ -773,7 +773,7 @@ function GroupView({
|
|||||||
export function LibraryPage() {
|
export function LibraryPage() {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { load, processes, groups, settings } = useLibraryStore()
|
const { load, processes, groups, settings } = useLibraryStore()
|
||||||
const { user } = useAuth()
|
const { user, isProfileLoaded } = useAuth()
|
||||||
const [view, setView] = useState<'home' | 'group'>('home')
|
const [view, setView] = useState<'home' | 'group'>('home')
|
||||||
const [activeGroupId, setActiveGroupId] = useState<string | null>(null)
|
const [activeGroupId, setActiveGroupId] = useState<string | null>(null)
|
||||||
const [transitioning, setTransitioning] = useState(false)
|
const [transitioning, setTransitioning] = useState(false)
|
||||||
@@ -811,10 +811,31 @@ export function LibraryPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentUserId = user!.id
|
// Esperar a que syncProfileFromDB complete antes de renderizar UI rol-dependiente.
|
||||||
const currentUserName = user!.name
|
// Evita el flash donde 'guest' se muestra como platform_admin durante la ventana de sync.
|
||||||
|
if (!user || !isProfileLoaded) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-background">
|
||||||
|
<AppHeader />
|
||||||
|
<div className="max-w-4xl mx-auto px-6 py-8">
|
||||||
|
<div className="space-y-5 animate-pulse">
|
||||||
|
<div className="h-4 w-32 bg-slate-100 rounded" />
|
||||||
|
<div className="grid gap-2.5" style={{ gridTemplateColumns: 'repeat(auto-fill, minmax(160px, 1fr))' }}>
|
||||||
|
{[1, 2, 3].map((i) => <div key={i} className="h-20 rounded-xl bg-slate-100" />)}
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{[1, 2].map((i) => <div key={i} className="h-14 rounded-xl bg-slate-100" />)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentUserId = user.id
|
||||||
|
const currentUserName = user.name
|
||||||
const totalGroups = groups.length
|
const totalGroups = groups.length
|
||||||
const isClientRole = user?.platformRole === 'client_editor' || user?.platformRole === 'client_viewer'
|
const isClientRole = user.platformRole === 'client_editor' || user.platformRole === 'client_viewer'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-background">
|
<div className="min-h-screen bg-background">
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ const CURRENCIES = [
|
|||||||
export function GlobalSettingsPanel() {
|
export function GlobalSettingsPanel() {
|
||||||
const { currentProcess, updateProcess } = useProcessStore()
|
const { currentProcess, updateProcess } = useProcessStore()
|
||||||
const { user } = useAuth()
|
const { user } = useAuth()
|
||||||
|
const isClientRole = user?.platformRole === 'client_editor' || user?.platformRole === 'client_viewer'
|
||||||
const [localName, setLocalName] = useState('')
|
const [localName, setLocalName] = useState('')
|
||||||
const [localClient, setLocalClient] = useState('')
|
const [localClient, setLocalClient] = useState('')
|
||||||
const [localCurrency, setLocalCurrency] = useState('USD')
|
const [localCurrency, setLocalCurrency] = useState('USD')
|
||||||
@@ -83,7 +84,10 @@ export function GlobalSettingsPanel() {
|
|||||||
|
|
||||||
{/* Cliente */}
|
{/* Cliente */}
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<Label htmlFor="client-name" className="text-xs font-medium">Cliente</Label>
|
<Label className="text-xs font-medium">Cliente</Label>
|
||||||
|
{isClientRole ? (
|
||||||
|
<p className="text-sm text-slate-600 h-8 flex items-center px-1">{localClient || '—'}</p>
|
||||||
|
) : (
|
||||||
<Input
|
<Input
|
||||||
id="client-name"
|
id="client-name"
|
||||||
value={localClient}
|
value={localClient}
|
||||||
@@ -91,6 +95,7 @@ export function GlobalSettingsPanel() {
|
|||||||
placeholder="ej. Empresa ABC"
|
placeholder="ej. Empresa ABC"
|
||||||
className="h-8 text-sm"
|
className="h-8 text-sm"
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Moneda */}
|
{/* Moneda */}
|
||||||
|
|||||||
@@ -235,8 +235,12 @@ export function WorkspacePage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Cliente — editable inline */}
|
{/* Cliente — editable inline solo para roles internos */}
|
||||||
{editingField === 'client' ? (
|
{user?.platformRole === 'client_editor' ? (
|
||||||
|
currentProcess.clientName && (
|
||||||
|
<p className="text-xs text-slate-400 truncate">{currentProcess.clientName}</p>
|
||||||
|
)
|
||||||
|
) : editingField === 'client' ? (
|
||||||
<input
|
<input
|
||||||
autoFocus
|
autoFocus
|
||||||
className="text-xs text-slate-400 bg-transparent border-b border-[#F59845] outline-none w-full"
|
className="text-xs text-slate-400 bg-transparent border-b border-[#F59845] outline-none w-full"
|
||||||
|
|||||||
11
supabase/migrations/016_add_simulations_update_policy.sql
Normal file
11
supabase/migrations/016_add_simulations_update_policy.sql
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
-- Fix: faltaba política UPDATE en simulations.
|
||||||
|
-- simulations_insert existe en 015 pero simulations_update no fue creada.
|
||||||
|
-- Sin esta policy, client_editor no puede guardar configuración sobre simulaciones existentes.
|
||||||
|
|
||||||
|
DROP POLICY IF EXISTS "simulations_update" ON public.simulations;
|
||||||
|
|
||||||
|
CREATE POLICY "simulations_update" ON public.simulations
|
||||||
|
FOR UPDATE USING (
|
||||||
|
EXISTS (SELECT 1 FROM public.processes WHERE id = simulations.process_id)
|
||||||
|
AND public.current_platform_role() NOT IN ('client_viewer', 'guest')
|
||||||
|
);
|
||||||
@@ -17,6 +17,7 @@ const { authUserRef, libStoreRef } = vi.hoisted(() => {
|
|||||||
name: 'Test User',
|
name: 'Test User',
|
||||||
platformRole: 'platform_admin' as string,
|
platformRole: 'platform_admin' as string,
|
||||||
},
|
},
|
||||||
|
isProfileLoaded: true,
|
||||||
}
|
}
|
||||||
const sampleProcess = {
|
const sampleProcess = {
|
||||||
id: 'proc-1',
|
id: 'proc-1',
|
||||||
@@ -59,7 +60,7 @@ const { authUserRef, libStoreRef } = vi.hoisted(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
vi.mock('@/auth/AuthContext', () => ({
|
vi.mock('@/auth/AuthContext', () => ({
|
||||||
useAuth: () => ({ user: authUserRef.current }),
|
useAuth: () => ({ user: authUserRef.current, isProfileLoaded: authUserRef.isProfileLoaded }),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/store/library-store', () => ({
|
vi.mock('@/store/library-store', () => ({
|
||||||
@@ -89,6 +90,24 @@ describe('LibraryPage — visibilidad de controles según rol', () => {
|
|||||||
libStoreRef.load = vi.fn()
|
libStoreRef.load = vi.fn()
|
||||||
libStoreRef.getAllTags = vi.fn().mockReturnValue([])
|
libStoreRef.getAllTags = vi.fn().mockReturnValue([])
|
||||||
libStoreRef.getLatestSimulation = vi.fn().mockResolvedValue(null)
|
libStoreRef.getLatestSimulation = vi.fn().mockResolvedValue(null)
|
||||||
|
authUserRef.isProfileLoaded = true
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('isProfileLoaded = false (sync en curso)', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
authUserRef.current = { ...authUserRef.current, platformRole: 'platform_admin' }
|
||||||
|
authUserRef.isProfileLoaded = false
|
||||||
|
})
|
||||||
|
|
||||||
|
it('NO muestra el botón "Importar BPMN" mientras el perfil no está cargado', () => {
|
||||||
|
render(<LibraryPage />)
|
||||||
|
expect(screen.queryByText('Importar BPMN')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('NO muestra el panel de grupos mientras el perfil no está cargado', () => {
|
||||||
|
render(<LibraryPage />)
|
||||||
|
expect(screen.queryByTestId('groups-header')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('platform_admin', () => {
|
describe('platform_admin', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user