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, 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 = () => {}
}