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, FormLabel,
FormMessage, FormMessage,
} from '@/components/ui/form' } from '@/components/ui/form'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { TIPO_MEDIO_OPTIONS } from '../tipoMedio' import { TIPO_MEDIO_OPTIONS } from '../tipoMedio'
import type { MedioDetail } from '../types' import type { MedioDetail } from '../types'
@@ -136,22 +143,24 @@ export function MedioForm({ initialData, isPending, error, onSubmit }: MedioForm
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Tipo</FormLabel> <FormLabel>Tipo</FormLabel>
<FormControl> <Select
<select value={field.value ? String(field.value) : ''}
{...field} onValueChange={(v) => field.onChange(v === '' ? '' : Number(v))}
value={field.value ?? ''} disabled={isPending}
disabled={isPending} >
aria-label="Tipo" <FormControl>
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" <SelectTrigger className="w-full" aria-label="Tipo">
> <SelectValue placeholder="Seleccioná un tipo" />
<option value="">Seleccioná un tipo</option> </SelectTrigger>
</FormControl>
<SelectContent>
{TIPO_MEDIO_OPTIONS.map((o) => ( {TIPO_MEDIO_OPTIONS.map((o) => (
<option key={o.value} value={o.value}> <SelectItem key={o.value} value={String(o.value)}>
{o.label} {o.label}
</option> </SelectItem>
))} ))}
</select> </SelectContent>
</FormControl> </Select>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}

View File

@@ -5,6 +5,13 @@ import { Input } from '@/components/ui/input'
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from '@/components/ui/skeleton'
import { CanPerform } from '@/components/auth/CanPerform' import { CanPerform } from '@/components/auth/CanPerform'
import { useDebouncedValue } from '@/hooks/useDebouncedValue' import { useDebouncedValue } from '@/hooks/useDebouncedValue'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { MediosTable } from '../components/MediosTable' import { MediosTable } from '../components/MediosTable'
import { useMediosList } from '../hooks/useMediosList' import { useMediosList } from '../hooks/useMediosList'
import { TIPO_MEDIO_OPTIONS } from '../tipoMedio' import { TIPO_MEDIO_OPTIONS } from '../tipoMedio'
@@ -70,28 +77,36 @@ export function MediosListPage() {
aria-label="Buscar medios" aria-label="Buscar medios"
/> />
<select <Select
aria-label="Tipo" value={tipo !== undefined ? String(tipo) : '__all__'}
onChange={(e) => handleTipoChange(e.target.value)} onValueChange={(v) => handleTipoChange(v === '__all__' ? '' : 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"
> >
<option value="">Todos los tipos</option> <SelectTrigger className="h-9 w-40" aria-label="Tipo">
{TIPO_MEDIO_OPTIONS.map((o) => ( <SelectValue placeholder="Todos los tipos" />
<option key={o.value} value={o.value}> </SelectTrigger>
{o.label} <SelectContent>
</option> <SelectItem value="__all__">Todos los tipos</SelectItem>
))} {TIPO_MEDIO_OPTIONS.map((o) => (
</select> <SelectItem key={o.value} value={String(o.value)}>
{o.label}
</SelectItem>
))}
</SelectContent>
</Select>
<select <Select
aria-label="Estado" value={activo !== undefined ? String(activo) : '__all__'}
onChange={(e) => handleActivoChange(e.target.value)} onValueChange={(v) => handleActivoChange(v === '__all__' ? '' : 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"
> >
<option value="">Todos</option> <SelectTrigger className="h-9 w-32" aria-label="Estado">
<option value="true">Activos</option> <SelectValue placeholder="Todos" />
<option value="false">Inactivos</option> </SelectTrigger>
</select> <SelectContent>
<SelectItem value="__all__">Todos</SelectItem>
<SelectItem value="true">Activos</SelectItem>
<SelectItem value="false">Inactivos</SelectItem>
</SelectContent>
</Select>
</div> </div>
{isLoading ? ( {isLoading ? (

View File

@@ -6,6 +6,13 @@ import {
CardHeader, CardHeader,
CardTitle, CardTitle,
} from '@/components/ui/card' } from '@/components/ui/card'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { useRoles } from '../../roles/hooks/useRoles' import { useRoles } from '../../roles/hooks/useRoles'
import { RolPermisosEditor } from '../components/RolPermisosEditor' import { RolPermisosEditor } from '../components/RolPermisosEditor'
@@ -28,28 +35,28 @@ export function RolPermisosPage() {
<CardContent className="space-y-6"> <CardContent className="space-y-6">
{/* Selector de rol */} {/* Selector de rol */}
<div className="flex flex-col gap-1 max-w-xs"> <div className="flex flex-col gap-1 max-w-xs">
<label <label className="text-sm font-medium text-foreground">
htmlFor="rol-selector"
className="text-sm font-medium text-foreground"
>
Rol Rol
</label> </label>
{loadingRoles ? ( {loadingRoles ? (
<p className="text-sm text-muted-foreground">Cargando roles...</p> <p className="text-sm text-muted-foreground">Cargando roles...</p>
) : ( ) : (
<select <Select
id="rol-selector" value={selectedRol ?? '__none__'}
value={selectedRol ?? ''} onValueChange={(v) => setSelectedRol(v === '__none__' ? null : v)}
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"
> >
<option value=""> Seleccioná un rol </option> <SelectTrigger className="w-full" aria-label="Rol">
{rolesActivos.map((r) => ( <SelectValue placeholder="— Seleccioná un rol —" />
<option key={r.codigo} value={r.codigo}> </SelectTrigger>
{r.nombre} ({r.codigo}) <SelectContent>
</option> <SelectItem value="__none__"> Seleccioná un rol </SelectItem>
))} {rolesActivos.map((r) => (
</select> <SelectItem key={r.codigo} value={r.codigo}>
{r.nombre} ({r.codigo})
</SelectItem>
))}
</SelectContent>
</Select>
)} )}
</div> </div>

View File

@@ -15,6 +15,13 @@ import {
FormLabel, FormLabel,
FormMessage, FormMessage,
} from '@/components/ui/form' } from '@/components/ui/form'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { useMediosList } from '@/features/medios/hooks/useMediosList' import { useMediosList } from '@/features/medios/hooks/useMediosList'
import { TIPO_SECCION_OPTIONS } from '../tipoSeccion' import { TIPO_SECCION_OPTIONS } from '../tipoSeccion'
import type { SeccionDetail, TipoSeccion } from '../types' import type { SeccionDetail, TipoSeccion } from '../types'
@@ -100,22 +107,24 @@ export function SeccionForm({ initialData, isPending, error, onSubmit }: Seccion
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Medio</FormLabel> <FormLabel>Medio</FormLabel>
<FormControl> <Select
<select value={field.value ? String(field.value) : ''}
{...field} onValueChange={(v) => field.onChange(Number(v))}
value={field.value ?? ''} disabled={isPending || isEdit}
disabled={isPending || isEdit} >
aria-label="Medio" <FormControl>
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" <SelectTrigger className="w-full" aria-label="Medio">
> <SelectValue placeholder="Seleccioná un medio" />
<option value="">Seleccioná un medio</option> </SelectTrigger>
</FormControl>
<SelectContent>
{medios.map((m) => ( {medios.map((m) => (
<option key={m.id} value={m.id}> <SelectItem key={m.id} value={String(m.id)}>
{m.nombre} {m.nombre}
</option> </SelectItem>
))} ))}
</select> </SelectContent>
</FormControl> </Select>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
@@ -165,22 +174,24 @@ export function SeccionForm({ initialData, isPending, error, onSubmit }: Seccion
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Tipo</FormLabel> <FormLabel>Tipo</FormLabel>
<FormControl> <Select
<select value={field.value ?? ''}
{...field} onValueChange={(v) => field.onChange(v as TipoSeccion)}
value={field.value ?? ''} disabled={isPending}
disabled={isPending} >
aria-label="Tipo de sección" <FormControl>
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" <SelectTrigger className="w-full" aria-label="Tipo de sección">
> <SelectValue placeholder="Seleccioná un tipo" />
<option value="">Seleccioná un tipo</option> </SelectTrigger>
</FormControl>
<SelectContent>
{TIPO_SECCION_OPTIONS.map((o) => ( {TIPO_SECCION_OPTIONS.map((o) => (
<option key={o.value} value={o.value}> <SelectItem key={o.value} value={o.value}>
{o.label} {o.label}
</option> </SelectItem>
))} ))}
</select> </SelectContent>
</FormControl> </Select>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}

View File

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

View File

@@ -14,6 +14,13 @@ import {
FormLabel, FormLabel,
FormMessage, FormMessage,
} from '@/components/ui/form' } from '@/components/ui/form'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { useCreateUser } from '../hooks/useCreateUser' import { useCreateUser } from '../hooks/useCreateUser'
import { useRolesForSelect } from '../hooks/useRolesForSelect' import { useRolesForSelect } from '../hooks/useRolesForSelect'
import type { CreatedUserDto } from '../api/createUser' import type { CreatedUserDto } from '../api/createUser'
@@ -202,23 +209,26 @@ export function UserForm({ onSuccess }: UserFormProps) {
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Rol</FormLabel> <FormLabel>Rol</FormLabel>
<FormControl> <Select
<select value={field.value}
{...field} onValueChange={field.onChange}
disabled={disabled || rolesError} 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" <FormControl>
> <SelectTrigger className="w-full" aria-label="Rol">
<option value=""> <SelectValue
{rolesLoading ? 'Cargando roles...' : 'Seleccioná un rol'} placeholder={rolesLoading ? 'Cargando roles...' : 'Seleccioná un rol'}
</option> />
</SelectTrigger>
</FormControl>
<SelectContent>
{rolOptions.map((r) => ( {rolOptions.map((r) => (
<option key={r.codigo} value={r.codigo}> <SelectItem key={r.codigo} value={r.codigo}>
{r.nombre} {r.nombre}
</option> </SelectItem>
))} ))}
</select> </SelectContent>
</FormControl> </Select>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}

View File

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

View File

@@ -17,6 +17,13 @@ import {
FormLabel, FormLabel,
FormMessage, FormMessage,
} from '@/components/ui/form' } from '@/components/ui/form'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { useUser } from '../hooks/useUser' import { useUser } from '../hooks/useUser'
import { useUpdateUser } from '../hooks/useUpdateUser' import { useUpdateUser } from '../hooks/useUpdateUser'
import { ResetPasswordModal } from '../components/ResetPasswordModal' import { ResetPasswordModal } from '../components/ResetPasswordModal'
@@ -199,18 +206,22 @@ export function UserEditPage() {
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Rol</FormLabel> <FormLabel>Rol</FormLabel>
<FormControl> <Select
<select value={field.value}
{...field} onValueChange={field.onChange}
disabled={isPending} 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" <FormControl>
> <SelectTrigger className="w-full" aria-label="Rol">
<option value="admin">Admin</option> <SelectValue placeholder="Seleccioná un rol" />
<option value="cajero">Cajero</option> </SelectTrigger>
<option value="reportes">Reportes</option> </FormControl>
</select> <SelectContent>
</FormControl> <SelectItem value="admin">Admin</SelectItem>
<SelectItem value="cajero">Cajero</SelectItem>
<SelectItem value="reportes">Reportes</SelectItem>
</SelectContent>
</Select>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}

View File

@@ -61,7 +61,11 @@ describe('CreateMedioPage', () => {
await userEvent.type(screen.getByLabelText(/código/i), 'RAD01') await userEvent.type(screen.getByLabelText(/código/i), 'RAD01')
await userEvent.type(screen.getByLabelText(/nombre/i), 'Radio AM') 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 userEvent.click(screen.getByRole('button', { name: /crear medio/i }))
await waitFor(() => expect(mockNavigate).toHaveBeenCalledWith('/admin/medios')) 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(/código/i), 'DUP01')
await userEvent.type(screen.getByLabelText(/nombre/i), 'Duplicado') 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 userEvent.click(screen.getByRole('button', { name: /crear medio/i }))
await waitFor(() => 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(/código/i), 'DIA99')
await userEvent.type(screen.getByLabelText(/nombre/i), 'Mi Diario') 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 userEvent.click(screen.getByRole('button', { name: /crear medio/i }))
await waitFor(() => { await waitFor(() => {

View File

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

View File

@@ -88,15 +88,25 @@ describe('SeccionForm — create mode', () => {
const onSubmit = vi.fn() const onSubmit = vi.fn()
renderForm({ onSubmit }) 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(() => await waitFor(() =>
expect(screen.getByRole('option', { name: 'Diario El Día' })).toBeInTheDocument(), 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(/código/i), 'CLAS99')
await userEvent.type(screen.getByLabelText(/nombre/i), 'Mi Sección') 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 userEvent.click(screen.getByRole('button', { name: /crear sección/i }))
await waitFor(() => { await waitFor(() => {
@@ -117,8 +127,9 @@ describe('SeccionForm — edit mode', () => {
renderForm({ initialData: sampleSeccion }) renderForm({ initialData: sampleSeccion })
const codigoInput = screen.getByLabelText(/código/i) as HTMLInputElement const codigoInput = screen.getByLabelText(/código/i) as HTMLInputElement
expect(codigoInput.disabled).toBe(true) expect(codigoInput.disabled).toBe(true)
const medioSelect = screen.getByLabelText(/medio/i) as HTMLSelectElement // Radix Select trigger is a <button> — check it's disabled
expect(medioSelect.disabled).toBe(true) const medioTrigger = screen.getByRole('combobox', { name: /medio/i })
expect(medioTrigger).toBeDisabled()
}) })
it('pre-fills form with initialData values', async () => { it('pre-fills form with initialData values', async () => {

View File

@@ -56,6 +56,10 @@ describe('SeccionesFilters', () => {
it('loads medios options from API', async () => { it('loads medios options from API', async () => {
renderFilters() 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(() => await waitFor(() =>
expect(screen.getByRole('option', { name: 'Diario El Día' })).toBeInTheDocument(), expect(screen.getByRole('option', { name: 'Diario El Día' })).toBeInTheDocument(),
) )
@@ -66,34 +70,55 @@ describe('SeccionesFilters', () => {
it('calls onMedioIdChange when medio is selected', async () => { it('calls onMedioIdChange when medio is selected', async () => {
const handlers = renderFilters() 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(() => await waitFor(() =>
expect(screen.getByRole('option', { name: 'Diario El Día' })).toBeInTheDocument(), 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) expect(handlers.onMedioIdChange).toHaveBeenCalledWith(1)
}) })
it('calls onTipoChange when tipo is selected', async () => { it('calls onTipoChange when tipo is selected', async () => {
const handlers = renderFilters() 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(() => await waitFor(() =>
expect(screen.getByRole('option', { name: 'Clasificados' })).toBeInTheDocument(), 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') expect(handlers.onTipoChange).toHaveBeenCalledWith('clasificados')
}) })
it('calls onActivoChange when estado is selected', async () => { it('calls onActivoChange when estado is selected', async () => {
const handlers = renderFilters() 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) expect(handlers.onActivoChange).toHaveBeenCalledWith(true)
}) })
it('renders all tipo options', async () => { it('renders all tipo options', async () => {
renderFilters() 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(() => await waitFor(() =>
expect(screen.getByRole('option', { name: 'Clasificados' })).toBeInTheDocument(), expect(screen.getByRole('option', { name: 'Clasificados' })).toBeInTheDocument(),
) )

View File

@@ -107,6 +107,13 @@ describe('UserEditPage', () => {
await userEvent.clear(nombreInput) await userEvent.clear(nombreInput)
await userEvent.type(nombreInput, 'Pedro') 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 // Submit
await userEvent.click(screen.getByRole('button', { name: /guardar|actualizar|save/i })) await userEvent.click(screen.getByRole('button', { name: /guardar|actualizar|save/i }))
@@ -129,6 +136,12 @@ describe('UserEditPage', () => {
await waitFor(() => expect(screen.getByDisplayValue('Juan')).toBeInTheDocument()) 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 userEvent.click(screen.getByRole('button', { name: /guardar|actualizar|save/i }))
await waitFor(() => await waitFor(() =>

View File

@@ -109,6 +109,10 @@ describe('UserForm — roles dropdown integration', () => {
) )
renderForm() 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. // Wait for roles to load — active options should appear.
await waitFor(() => { await waitFor(() => {
expect( expect(
@@ -132,15 +136,21 @@ describe('UserForm — roles dropdown integration', () => {
const user = userEvent.setup() const user = userEvent.setup()
renderForm(onSuccess) 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(() => { await waitFor(() => {
expect(screen.getByRole('option', { name: 'Cajero' })).toBeInTheDocument() 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(/usuario/i), 'jdoe123')
await user.type(screen.getByLabelText(/^contraseña$/i), 'Secret12') await user.type(screen.getByLabelText(/^contraseña$/i), 'Secret12')
await user.type(screen.getByLabelText(/nombre/i), 'Juan') await user.type(screen.getByLabelText(/nombre/i), 'Juan')
await user.type(screen.getByLabelText(/apellido/i), 'Doe') 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 })) await user.click(screen.getByRole('button', { name: /crear usuario/i }))
@@ -176,15 +186,20 @@ describe('UserForm — backend error display', () => {
const user = userEvent.setup() const user = userEvent.setup()
renderForm() renderForm()
// Open trigger, wait for options, pick Cajero
const rolTrigger = screen.getByRole('combobox', { name: /rol/i })
await user.click(rolTrigger)
await waitFor(() => { await waitFor(() => {
expect(screen.getByRole('option', { name: 'Cajero' })).toBeInTheDocument() 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(/usuario/i), 'existing')
await user.type(screen.getByLabelText(/^contraseña$/i), 'Secret12') await user.type(screen.getByLabelText(/^contraseña$/i), 'Secret12')
await user.type(screen.getByLabelText(/nombre/i), 'Juan') await user.type(screen.getByLabelText(/nombre/i), 'Juan')
await user.type(screen.getByLabelText(/apellido/i), 'Doe') 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 })) 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)) await waitFor(() => expect(requests.length).toBeGreaterThan(0))
const rolSelect = screen.getByRole('combobox', { name: /rol/i }) // Open the Radix Select trigger and pick "Admin"
await userEvent.selectOptions(rolSelect, 'admin') const rolTrigger = screen.getByRole('combobox', { name: /rol/i })
await userEvent.click(rolTrigger)
await userEvent.click(screen.getByRole('option', { name: /^admin$/i }))
await waitFor(() => { await waitFor(() => {
const filtered = requests.find((u) => u.includes('rol=admin')) const filtered = requests.find((u) => u.includes('rol=admin'))

View File

@@ -1 +1,10 @@
import '@testing-library/jest-dom' 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 = () => {}
}