fix(web): reemplazar <select> nativos por shadcn Select (dark mode compat) — ADM-001

Reemplaza 13 <select>/<option> nativos en 8 archivos por el componente
shadcn Select (Radix UI). Los selects nativos ignoraban los tokens del
design system en dark mode, causando texto invisible. Se agrega mock de
pointer capture APIs en test setup para compatibilidad de Radix con jsdom.
This commit is contained in:
2026-04-17 10:13:20 -03:00
parent 6b946f6080
commit 740298a9e1
17 changed files with 352 additions and 175 deletions

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 ?? ''}
<Select
value={field.value ? String(field.value) : ''}
onValueChange={(v) => field.onChange(v === '' ? '' : Number(v))}
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>
{TIPO_MEDIO_OPTIONS.map((o) => (
<option key={o.value} value={o.value}>
{o.label}
</option>
))}
</select>
<FormControl>
<SelectTrigger className="w-full" aria-label="Tipo">
<SelectValue placeholder="Seleccioná un tipo" />
</SelectTrigger>
</FormControl>
<SelectContent>
{TIPO_MEDIO_OPTIONS.map((o) => (
<SelectItem key={o.value} value={String(o.value)}>
{o.label}
</SelectItem>
))}
</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>
<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) => (
<option key={o.value} value={o.value}>
<SelectItem key={o.value} value={String(o.value)}>
{o.label}
</option>
</SelectItem>
))}
</select>
</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>
<SelectTrigger className="w-full" aria-label="Rol">
<SelectValue placeholder="— Seleccioná un rol —" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__none__"> Seleccioná un rol </SelectItem>
{rolesActivos.map((r) => (
<option key={r.codigo} value={r.codigo}>
<SelectItem key={r.codigo} value={r.codigo}>
{r.nombre} ({r.codigo})
</option>
</SelectItem>
))}
</select>
</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 ?? ''}
<Select
value={field.value ? String(field.value) : ''}
onValueChange={(v) => field.onChange(Number(v))}
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>
{medios.map((m) => (
<option key={m.id} value={m.id}>
{m.nombre}
</option>
))}
</select>
<FormControl>
<SelectTrigger className="w-full" aria-label="Medio">
<SelectValue placeholder="Seleccioná un medio" />
</SelectTrigger>
</FormControl>
<SelectContent>
{medios.map((m) => (
<SelectItem key={m.id} value={String(m.id)}>
{m.nombre}
</SelectItem>
))}
</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}
<Select
value={field.value ?? ''}
onValueChange={(v) => field.onChange(v as TipoSeccion)}
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>
{TIPO_SECCION_OPTIONS.map((o) => (
<option key={o.value} value={o.value}>
{o.label}
</option>
))}
</select>
<FormControl>
<SelectTrigger className="w-full" aria-label="Tipo de sección">
<SelectValue placeholder="Seleccioná un tipo" />
</SelectTrigger>
</FormControl>
<SelectContent>
{TIPO_SECCION_OPTIONS.map((o) => (
<SelectItem key={o.value} value={o.value}>
{o.label}
</SelectItem>
))}
</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>
<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) => (
<option key={m.id} value={m.id}>
<SelectItem key={m.id} value={String(m.id)}>
{m.nombre}
</option>
</SelectItem>
))}
</select>
</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>
<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) => (
<option key={o.value} value={o.value}>
<SelectItem key={o.value} value={o.value}>
{o.label}
</option>
</SelectItem>
))}
</select>
</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"
<Select
value={field.value}
onValueChange={field.onChange}
disabled={disabled || !!rolesError}
>
<option value="">
{rolesLoading ? 'Cargando roles...' : 'Seleccioná un rol'}
</option>
{rolOptions.map((r) => (
<option key={r.codigo} value={r.codigo}>
{r.nombre}
</option>
))}
</select>
<FormControl>
<SelectTrigger className="w-full" aria-label="Rol">
<SelectValue
placeholder={rolesLoading ? 'Cargando roles...' : 'Seleccioná un rol'}
/>
</SelectTrigger>
</FormControl>
<SelectContent>
{rolOptions.map((r) => (
<SelectItem key={r.codigo} value={r.codigo}>
{r.nombre}
</SelectItem>
))}
</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)}
>
<SelectTrigger className="h-9 w-40" aria-label="Rol">
<SelectValue placeholder="Todos los roles" />
</SelectTrigger>
<SelectContent>
{ROL_OPTIONS.map((r) => (
<option key={r.value} value={r.value}>
<SelectItem key={r.value || '__all__'} value={r.value || '__all__'}>
{r.label}
</option>
</SelectItem>
))}
</select>
</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}
<Select
value={field.value}
onValueChange={field.onChange}
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>
<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 = () => {}
}