ADM-001: Medios y Secciones (fundacional) #15

Merged
dmolinari merged 9 commits from feature/ADM-001 into main 2026-04-17 14:37:15 +00:00
17 changed files with 352 additions and 175 deletions
Showing only changes of commit 740298a9e1 - Show all commits

View File

@@ -15,6 +15,13 @@ import {
FormLabel,
FormMessage,
} from '@/components/ui/form'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { TIPO_MEDIO_OPTIONS } from '../tipoMedio'
import type { MedioDetail } from '../types'
@@ -136,22 +143,24 @@ export function MedioForm({ initialData, isPending, error, onSubmit }: MedioForm
render={({ field }) => (
<FormItem>
<FormLabel>Tipo</FormLabel>
<FormControl>
<select
{...field}
value={field.value ?? ''}
disabled={isPending}
aria-label="Tipo"
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
>
<option value="">Seleccioná un tipo</option>
<Select
value={field.value ? String(field.value) : ''}
onValueChange={(v) => field.onChange(v === '' ? '' : Number(v))}
disabled={isPending}
>
<FormControl>
<SelectTrigger className="w-full" aria-label="Tipo">
<SelectValue placeholder="Seleccioná un tipo" />
</SelectTrigger>
</FormControl>
<SelectContent>
{TIPO_MEDIO_OPTIONS.map((o) => (
<option key={o.value} value={o.value}>
<SelectItem key={o.value} value={String(o.value)}>
{o.label}
</option>
</SelectItem>
))}
</select>
</FormControl>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}

View File

@@ -5,6 +5,13 @@ import { Input } from '@/components/ui/input'
import { Skeleton } from '@/components/ui/skeleton'
import { CanPerform } from '@/components/auth/CanPerform'
import { useDebouncedValue } from '@/hooks/useDebouncedValue'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { MediosTable } from '../components/MediosTable'
import { useMediosList } from '../hooks/useMediosList'
import { TIPO_MEDIO_OPTIONS } from '../tipoMedio'
@@ -70,28 +77,36 @@ export function MediosListPage() {
aria-label="Buscar medios"
/>
<select
aria-label="Tipo"
onChange={(e) => handleTipoChange(e.target.value)}
className="flex h-9 rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
<Select
value={tipo !== undefined ? String(tipo) : '__all__'}
onValueChange={(v) => handleTipoChange(v === '__all__' ? '' : v)}
>
<option value="">Todos los tipos</option>
{TIPO_MEDIO_OPTIONS.map((o) => (
<option key={o.value} value={o.value}>
{o.label}
</option>
))}
</select>
<SelectTrigger className="h-9 w-40" aria-label="Tipo">
<SelectValue placeholder="Todos los tipos" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__all__">Todos los tipos</SelectItem>
{TIPO_MEDIO_OPTIONS.map((o) => (
<SelectItem key={o.value} value={String(o.value)}>
{o.label}
</SelectItem>
))}
</SelectContent>
</Select>
<select
aria-label="Estado"
onChange={(e) => handleActivoChange(e.target.value)}
className="flex h-9 rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
<Select
value={activo !== undefined ? String(activo) : '__all__'}
onValueChange={(v) => handleActivoChange(v === '__all__' ? '' : v)}
>
<option value="">Todos</option>
<option value="true">Activos</option>
<option value="false">Inactivos</option>
</select>
<SelectTrigger className="h-9 w-32" aria-label="Estado">
<SelectValue placeholder="Todos" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__all__">Todos</SelectItem>
<SelectItem value="true">Activos</SelectItem>
<SelectItem value="false">Inactivos</SelectItem>
</SelectContent>
</Select>
</div>
{isLoading ? (

View File

@@ -6,6 +6,13 @@ import {
CardHeader,
CardTitle,
} from '@/components/ui/card'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { useRoles } from '../../roles/hooks/useRoles'
import { RolPermisosEditor } from '../components/RolPermisosEditor'
@@ -28,28 +35,28 @@ export function RolPermisosPage() {
<CardContent className="space-y-6">
{/* Selector de rol */}
<div className="flex flex-col gap-1 max-w-xs">
<label
htmlFor="rol-selector"
className="text-sm font-medium text-foreground"
>
<label className="text-sm font-medium text-foreground">
Rol
</label>
{loadingRoles ? (
<p className="text-sm text-muted-foreground">Cargando roles...</p>
) : (
<select
id="rol-selector"
value={selectedRol ?? ''}
onChange={(e) => setSelectedRol(e.target.value || null)}
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
<Select
value={selectedRol ?? '__none__'}
onValueChange={(v) => setSelectedRol(v === '__none__' ? null : v)}
>
<option value=""> Seleccioná un rol </option>
{rolesActivos.map((r) => (
<option key={r.codigo} value={r.codigo}>
{r.nombre} ({r.codigo})
</option>
))}
</select>
<SelectTrigger className="w-full" aria-label="Rol">
<SelectValue placeholder="— Seleccioná un rol —" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__none__"> Seleccioná un rol </SelectItem>
{rolesActivos.map((r) => (
<SelectItem key={r.codigo} value={r.codigo}>
{r.nombre} ({r.codigo})
</SelectItem>
))}
</SelectContent>
</Select>
)}
</div>

View File

@@ -15,6 +15,13 @@ import {
FormLabel,
FormMessage,
} from '@/components/ui/form'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { useMediosList } from '@/features/medios/hooks/useMediosList'
import { TIPO_SECCION_OPTIONS } from '../tipoSeccion'
import type { SeccionDetail, TipoSeccion } from '../types'
@@ -100,22 +107,24 @@ export function SeccionForm({ initialData, isPending, error, onSubmit }: Seccion
render={({ field }) => (
<FormItem>
<FormLabel>Medio</FormLabel>
<FormControl>
<select
{...field}
value={field.value ?? ''}
disabled={isPending || isEdit}
aria-label="Medio"
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
>
<option value="">Seleccioná un medio</option>
<Select
value={field.value ? String(field.value) : ''}
onValueChange={(v) => field.onChange(Number(v))}
disabled={isPending || isEdit}
>
<FormControl>
<SelectTrigger className="w-full" aria-label="Medio">
<SelectValue placeholder="Seleccioná un medio" />
</SelectTrigger>
</FormControl>
<SelectContent>
{medios.map((m) => (
<option key={m.id} value={m.id}>
<SelectItem key={m.id} value={String(m.id)}>
{m.nombre}
</option>
</SelectItem>
))}
</select>
</FormControl>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
@@ -165,22 +174,24 @@ export function SeccionForm({ initialData, isPending, error, onSubmit }: Seccion
render={({ field }) => (
<FormItem>
<FormLabel>Tipo</FormLabel>
<FormControl>
<select
{...field}
value={field.value ?? ''}
disabled={isPending}
aria-label="Tipo de sección"
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
>
<option value="">Seleccioná un tipo</option>
<Select
value={field.value ?? ''}
onValueChange={(v) => field.onChange(v as TipoSeccion)}
disabled={isPending}
>
<FormControl>
<SelectTrigger className="w-full" aria-label="Tipo de sección">
<SelectValue placeholder="Seleccioná un tipo" />
</SelectTrigger>
</FormControl>
<SelectContent>
{TIPO_SECCION_OPTIONS.map((o) => (
<option key={o.value} value={o.value}>
<SelectItem key={o.value} value={o.value}>
{o.label}
</option>
</SelectItem>
))}
</select>
</FormControl>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}

View File

@@ -1,6 +1,13 @@
import { useEffect, useState } from 'react'
import { Input } from '@/components/ui/input'
import { useDebouncedValue } from '@/hooks/useDebouncedValue'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { useMediosList } from '@/features/medios/hooks/useMediosList'
import { TIPO_SECCION_OPTIONS } from '../tipoSeccion'
import type { TipoSeccion } from '../types'
@@ -40,51 +47,56 @@ export function SeccionesFilters({
aria-label="Buscar secciones"
/>
<select
aria-label="Medio"
onChange={(e) => {
const v = e.target.value
onMedioIdChange(v === '' ? undefined : Number(v))
}}
className="flex h-9 rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
<Select
defaultValue="__all__"
onValueChange={(v) => onMedioIdChange(v === '__all__' ? undefined : Number(v))}
>
<option value="">Todos los medios</option>
{medios.map((m) => (
<option key={m.id} value={m.id}>
{m.nombre}
</option>
))}
</select>
<SelectTrigger className="h-9 w-44" aria-label="Medio">
<SelectValue placeholder="Todos los medios" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__all__">Todos los medios</SelectItem>
{medios.map((m) => (
<SelectItem key={m.id} value={String(m.id)}>
{m.nombre}
</SelectItem>
))}
</SelectContent>
</Select>
<select
aria-label="Tipo de sección"
onChange={(e) => {
const v = e.target.value
onTipoChange(v === '' ? undefined : (v as TipoSeccion))
}}
className="flex h-9 rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
<Select
defaultValue="__all__"
onValueChange={(v) => onTipoChange(v === '__all__' ? undefined : (v as TipoSeccion))}
>
<option value="">Todos los tipos</option>
{TIPO_SECCION_OPTIONS.map((o) => (
<option key={o.value} value={o.value}>
{o.label}
</option>
))}
</select>
<SelectTrigger className="h-9 w-44" aria-label="Tipo de sección">
<SelectValue placeholder="Todos los tipos" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__all__">Todos los tipos</SelectItem>
{TIPO_SECCION_OPTIONS.map((o) => (
<SelectItem key={o.value} value={o.value}>
{o.label}
</SelectItem>
))}
</SelectContent>
</Select>
<select
aria-label="Estado"
onChange={(e) => {
const v = e.target.value
if (v === '') onActivoChange(undefined)
<Select
defaultValue="__all__"
onValueChange={(v) => {
if (v === '__all__') onActivoChange(undefined)
else onActivoChange(v === 'true')
}}
className="flex h-9 rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
>
<option value="">Todos</option>
<option value="true">Activos</option>
<option value="false">Inactivos</option>
</select>
<SelectTrigger className="h-9 w-32" aria-label="Estado">
<SelectValue placeholder="Todos" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__all__">Todos</SelectItem>
<SelectItem value="true">Activos</SelectItem>
<SelectItem value="false">Inactivos</SelectItem>
</SelectContent>
</Select>
</div>
)
}

View File

@@ -14,6 +14,13 @@ import {
FormLabel,
FormMessage,
} from '@/components/ui/form'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { useCreateUser } from '../hooks/useCreateUser'
import { useRolesForSelect } from '../hooks/useRolesForSelect'
import type { CreatedUserDto } from '../api/createUser'
@@ -202,23 +209,26 @@ export function UserForm({ onSuccess }: UserFormProps) {
render={({ field }) => (
<FormItem>
<FormLabel>Rol</FormLabel>
<FormControl>
<select
{...field}
disabled={disabled || rolesError}
aria-label="Rol"
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
>
<option value="">
{rolesLoading ? 'Cargando roles...' : 'Seleccioná un rol'}
</option>
<Select
value={field.value}
onValueChange={field.onChange}
disabled={disabled || !!rolesError}
>
<FormControl>
<SelectTrigger className="w-full" aria-label="Rol">
<SelectValue
placeholder={rolesLoading ? 'Cargando roles...' : 'Seleccioná un rol'}
/>
</SelectTrigger>
</FormControl>
<SelectContent>
{rolOptions.map((r) => (
<option key={r.codigo} value={r.codigo}>
<SelectItem key={r.codigo} value={r.codigo}>
{r.nombre}
</option>
</SelectItem>
))}
</select>
</FormControl>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}

View File

@@ -1,6 +1,13 @@
import { useState, useEffect } from 'react'
import { Input } from '@/components/ui/input'
import { useDebouncedValue } from '@/hooks/useDebouncedValue'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
interface UsersFiltersProps {
onRolChange: (rol: string) => void
@@ -38,32 +45,39 @@ export function UsersFilters({ onRolChange, onActivoChange, onSearchChange }: Us
/>
{/* Rol select */}
<select
aria-label="Rol"
onChange={(e) => onRolChange(e.target.value)}
className="flex h-9 rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
<Select
defaultValue="__all__"
onValueChange={(v) => onRolChange(v === '__all__' ? '' : v)}
>
{ROL_OPTIONS.map((r) => (
<option key={r.value} value={r.value}>
{r.label}
</option>
))}
</select>
<SelectTrigger className="h-9 w-40" aria-label="Rol">
<SelectValue placeholder="Todos los roles" />
</SelectTrigger>
<SelectContent>
{ROL_OPTIONS.map((r) => (
<SelectItem key={r.value || '__all__'} value={r.value || '__all__'}>
{r.label}
</SelectItem>
))}
</SelectContent>
</Select>
{/* Activo filter */}
<select
aria-label="Estado"
onChange={(e) => {
const v = e.target.value
if (v === '') onActivoChange(undefined)
<Select
defaultValue="__all__"
onValueChange={(v) => {
if (v === '__all__') onActivoChange(undefined)
else onActivoChange(v === 'true')
}}
className="flex h-9 rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
>
<option value="">Todos</option>
<option value="true">Activos</option>
<option value="false">Inactivos</option>
</select>
<SelectTrigger className="h-9 w-32" aria-label="Estado">
<SelectValue placeholder="Todos" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__all__">Todos</SelectItem>
<SelectItem value="true">Activos</SelectItem>
<SelectItem value="false">Inactivos</SelectItem>
</SelectContent>
</Select>
</div>
)
}

View File

@@ -17,6 +17,13 @@ import {
FormLabel,
FormMessage,
} from '@/components/ui/form'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { useUser } from '../hooks/useUser'
import { useUpdateUser } from '../hooks/useUpdateUser'
import { ResetPasswordModal } from '../components/ResetPasswordModal'
@@ -199,18 +206,22 @@ export function UserEditPage() {
render={({ field }) => (
<FormItem>
<FormLabel>Rol</FormLabel>
<FormControl>
<select
{...field}
disabled={isPending}
aria-label="Rol"
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
>
<option value="admin">Admin</option>
<option value="cajero">Cajero</option>
<option value="reportes">Reportes</option>
</select>
</FormControl>
<Select
value={field.value}
onValueChange={field.onChange}
disabled={isPending}
>
<FormControl>
<SelectTrigger className="w-full" aria-label="Rol">
<SelectValue placeholder="Seleccioná un rol" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="admin">Admin</SelectItem>
<SelectItem value="cajero">Cajero</SelectItem>
<SelectItem value="reportes">Reportes</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}

View File

@@ -61,7 +61,11 @@ describe('CreateMedioPage', () => {
await userEvent.type(screen.getByLabelText(/código/i), 'RAD01')
await userEvent.type(screen.getByLabelText(/nombre/i), 'Radio AM')
await userEvent.selectOptions(screen.getByLabelText(/tipo/i), '2')
// Open Radix Select trigger and pick "Radio" (value=2)
await userEvent.click(screen.getByRole('combobox', { name: /tipo/i }))
await userEvent.click(screen.getByRole('option', { name: /^radio$/i }))
await userEvent.click(screen.getByRole('button', { name: /crear medio/i }))
await waitFor(() => expect(mockNavigate).toHaveBeenCalledWith('/admin/medios'))
@@ -81,7 +85,11 @@ describe('CreateMedioPage', () => {
await userEvent.type(screen.getByLabelText(/código/i), 'DUP01')
await userEvent.type(screen.getByLabelText(/nombre/i), 'Duplicado')
await userEvent.selectOptions(screen.getByLabelText(/tipo/i), '1')
// Open Radix Select trigger and pick "Diario" (value=1)
await userEvent.click(screen.getByRole('combobox', { name: /tipo/i }))
await userEvent.click(screen.getByRole('option', { name: /^diario$/i }))
await userEvent.click(screen.getByRole('button', { name: /crear medio/i }))
await waitFor(() =>

View File

@@ -79,7 +79,11 @@ describe('MedioForm — create mode', () => {
await userEvent.type(screen.getByLabelText(/código/i), 'DIA99')
await userEvent.type(screen.getByLabelText(/nombre/i), 'Mi Diario')
await userEvent.selectOptions(screen.getByLabelText(/tipo/i), '1')
// Open the Radix Select trigger and pick option
await userEvent.click(screen.getByRole('combobox', { name: /tipo/i }))
await userEvent.click(screen.getByRole('option', { name: /diario/i }))
await userEvent.click(screen.getByRole('button', { name: /crear medio/i }))
await waitFor(() => {

View File

@@ -144,8 +144,9 @@ describe('MediosListPage', () => {
await waitFor(() => expect(requests.length).toBeGreaterThan(0))
const tipoSelect = screen.getByRole('combobox', { name: /tipo/i })
await userEvent.selectOptions(tipoSelect, '1')
// Open the Radix Select trigger and pick "Diario" (value=1)
await userEvent.click(screen.getByRole('combobox', { name: /tipo/i }))
await userEvent.click(screen.getByRole('option', { name: /^diario$/i }))
await waitFor(() => {
const filtered = requests.find((u) => u.includes('tipo=1'))

View File

@@ -88,15 +88,25 @@ describe('SeccionForm — create mode', () => {
const onSubmit = vi.fn()
renderForm({ onSubmit })
// Wait for medios to load
// Open Medio trigger, wait for medios to load, then pick one
const medioTrigger = screen.getByRole('combobox', { name: /medio/i })
await userEvent.click(medioTrigger)
await waitFor(() =>
expect(screen.getByRole('option', { name: 'Diario El Día' })).toBeInTheDocument(),
)
await userEvent.click(screen.getByRole('option', { name: 'Diario El Día' }))
await userEvent.selectOptions(screen.getByLabelText(/medio/i), '1')
await userEvent.type(screen.getByLabelText(/código/i), 'CLAS99')
await userEvent.type(screen.getByLabelText(/nombre/i), 'Mi Sección')
await userEvent.selectOptions(screen.getByLabelText(/tipo de sección/i), 'clasificados')
// Open Tipo trigger and pick Clasificados
const tipoTrigger = screen.getByRole('combobox', { name: /tipo de sección/i })
await userEvent.click(tipoTrigger)
await waitFor(() =>
expect(screen.getByRole('option', { name: 'Clasificados' })).toBeInTheDocument(),
)
await userEvent.click(screen.getByRole('option', { name: 'Clasificados' }))
await userEvent.click(screen.getByRole('button', { name: /crear sección/i }))
await waitFor(() => {
@@ -117,8 +127,9 @@ describe('SeccionForm — edit mode', () => {
renderForm({ initialData: sampleSeccion })
const codigoInput = screen.getByLabelText(/código/i) as HTMLInputElement
expect(codigoInput.disabled).toBe(true)
const medioSelect = screen.getByLabelText(/medio/i) as HTMLSelectElement
expect(medioSelect.disabled).toBe(true)
// Radix Select trigger is a <button> — check it's disabled
const medioTrigger = screen.getByRole('combobox', { name: /medio/i })
expect(medioTrigger).toBeDisabled()
})
it('pre-fills form with initialData values', async () => {

View File

@@ -56,6 +56,10 @@ describe('SeccionesFilters', () => {
it('loads medios options from API', async () => {
renderFilters()
// Open the Medio Radix Select to see options in the portal
const medioTrigger = await screen.findByRole('combobox', { name: /medio/i })
await userEvent.click(medioTrigger)
await waitFor(() =>
expect(screen.getByRole('option', { name: 'Diario El Día' })).toBeInTheDocument(),
)
@@ -66,34 +70,55 @@ describe('SeccionesFilters', () => {
it('calls onMedioIdChange when medio is selected', async () => {
const handlers = renderFilters()
// Open Medio trigger, wait for options to load, then pick one
const medioTrigger = await screen.findByRole('combobox', { name: /medio/i })
await userEvent.click(medioTrigger)
await waitFor(() =>
expect(screen.getByRole('option', { name: 'Diario El Día' })).toBeInTheDocument(),
)
await userEvent.selectOptions(screen.getByLabelText(/medio/i), '1')
await userEvent.click(screen.getByRole('option', { name: 'Diario El Día' }))
expect(handlers.onMedioIdChange).toHaveBeenCalledWith(1)
})
it('calls onTipoChange when tipo is selected', async () => {
const handlers = renderFilters()
// Open Tipo de sección trigger and pick an option
const tipoTrigger = screen.getByRole('combobox', { name: /tipo de sección/i })
await userEvent.click(tipoTrigger)
await waitFor(() =>
expect(screen.getByRole('option', { name: 'Clasificados' })).toBeInTheDocument(),
)
await userEvent.selectOptions(screen.getByLabelText(/tipo de sección/i), 'clasificados')
await userEvent.click(screen.getByRole('option', { name: 'Clasificados' }))
expect(handlers.onTipoChange).toHaveBeenCalledWith('clasificados')
})
it('calls onActivoChange when estado is selected', async () => {
const handlers = renderFilters()
await userEvent.selectOptions(screen.getByLabelText(/estado/i), 'true')
// Open Estado trigger and pick Activos
const estadoTrigger = screen.getByRole('combobox', { name: /estado/i })
await userEvent.click(estadoTrigger)
await waitFor(() =>
expect(screen.getByRole('option', { name: 'Activos' })).toBeInTheDocument(),
)
await userEvent.click(screen.getByRole('option', { name: 'Activos' }))
expect(handlers.onActivoChange).toHaveBeenCalledWith(true)
})
it('renders all tipo options', async () => {
renderFilters()
// Open Tipo de sección trigger to see options in portal
const tipoTrigger = screen.getByRole('combobox', { name: /tipo de sección/i })
await userEvent.click(tipoTrigger)
await waitFor(() =>
expect(screen.getByRole('option', { name: 'Clasificados' })).toBeInTheDocument(),
)

View File

@@ -107,6 +107,13 @@ describe('UserEditPage', () => {
await userEvent.clear(nombreInput)
await userEvent.type(nombreInput, 'Pedro')
// Confirm the rol Select has a value by opening and re-selecting the prefilled value
// (Radix Select in jsdom needs interaction to register the item text in context)
const rolTrigger = screen.getByRole('combobox', { name: /rol/i })
await userEvent.click(rolTrigger)
await waitFor(() => expect(screen.getByRole('option', { name: /cajero/i })).toBeInTheDocument())
await userEvent.click(screen.getByRole('option', { name: /cajero/i }))
// Submit
await userEvent.click(screen.getByRole('button', { name: /guardar|actualizar|save/i }))
@@ -129,6 +136,12 @@ describe('UserEditPage', () => {
await waitFor(() => expect(screen.getByDisplayValue('Juan')).toBeInTheDocument())
// Confirm rol by opening and re-selecting the prefilled value
const rolTrigger = screen.getByRole('combobox', { name: /rol/i })
await userEvent.click(rolTrigger)
await waitFor(() => expect(screen.getByRole('option', { name: /cajero/i })).toBeInTheDocument())
await userEvent.click(screen.getByRole('option', { name: /cajero/i }))
await userEvent.click(screen.getByRole('button', { name: /guardar|actualizar|save/i }))
await waitFor(() =>

View File

@@ -109,6 +109,10 @@ describe('UserForm — roles dropdown integration', () => {
)
renderForm()
// Open the Radix Select trigger to see options in the portal
const rolTrigger = screen.getByRole('combobox', { name: /rol/i })
await userEvent.click(rolTrigger)
// Wait for roles to load — active options should appear.
await waitFor(() => {
expect(
@@ -132,15 +136,21 @@ describe('UserForm — roles dropdown integration', () => {
const user = userEvent.setup()
renderForm(onSuccess)
// Open trigger and wait for Cajero option to load
const rolTrigger = screen.getByRole('combobox', { name: /rol/i })
await user.click(rolTrigger)
await waitFor(() => {
expect(screen.getByRole('option', { name: 'Cajero' })).toBeInTheDocument()
})
// Pick Cajero option
await user.click(screen.getByRole('option', { name: 'Cajero' }))
await user.type(screen.getByLabelText(/usuario/i), 'jdoe123')
await user.type(screen.getByLabelText(/^contraseña$/i), 'Secret12')
await user.type(screen.getByLabelText(/nombre/i), 'Juan')
await user.type(screen.getByLabelText(/apellido/i), 'Doe')
await user.selectOptions(screen.getByLabelText(/rol/i), 'cajero')
await user.click(screen.getByRole('button', { name: /crear usuario/i }))
@@ -176,15 +186,20 @@ describe('UserForm — backend error display', () => {
const user = userEvent.setup()
renderForm()
// Open trigger, wait for options, pick Cajero
const rolTrigger = screen.getByRole('combobox', { name: /rol/i })
await user.click(rolTrigger)
await waitFor(() => {
expect(screen.getByRole('option', { name: 'Cajero' })).toBeInTheDocument()
})
await user.click(screen.getByRole('option', { name: 'Cajero' }))
await user.type(screen.getByLabelText(/usuario/i), 'existing')
await user.type(screen.getByLabelText(/^contraseña$/i), 'Secret12')
await user.type(screen.getByLabelText(/nombre/i), 'Juan')
await user.type(screen.getByLabelText(/apellido/i), 'Doe')
await user.selectOptions(screen.getByLabelText(/rol/i), 'cajero')
await user.click(screen.getByRole('button', { name: /crear usuario/i }))

View File

@@ -171,8 +171,10 @@ describe('UsersListPage', () => {
await waitFor(() => expect(requests.length).toBeGreaterThan(0))
const rolSelect = screen.getByRole('combobox', { name: /rol/i })
await userEvent.selectOptions(rolSelect, 'admin')
// Open the Radix Select trigger and pick "Admin"
const rolTrigger = screen.getByRole('combobox', { name: /rol/i })
await userEvent.click(rolTrigger)
await userEvent.click(screen.getByRole('option', { name: /^admin$/i }))
await waitFor(() => {
const filtered = requests.find((u) => u.includes('rol=admin'))

View File

@@ -1 +1,10 @@
import '@testing-library/jest-dom'
// Radix UI primitives use pointer capture APIs not available in jsdom.
// Provide no-op stubs so Radix Select/etc. work in unit tests.
if (typeof window !== 'undefined') {
window.HTMLElement.prototype.hasPointerCapture = () => false
window.HTMLElement.prototype.setPointerCapture = () => {}
window.HTMLElement.prototype.releasePointerCapture = () => {}
window.HTMLElement.prototype.scrollIntoView = () => {}
}