Files
FRASA-CAS/apps/alta-clientes/src/components/AltaAfluenciaForm.test.tsx

229 lines
10 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
import AltaAfluenciaForm from './AltaAfluenciaForm'
type FetchFn = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>
const respuesta = (data: unknown, status = 201) =>
new Response(JSON.stringify(data), { status, headers: { 'Content-Type': 'application/json' } })
let fetchMock: ReturnType<typeof vi.fn<FetchFn>>
beforeEach(() => {
fetchMock = vi.fn<FetchFn>()
vi.stubGlobal('fetch', fetchMock)
})
afterEach(() => {
cleanup()
vi.unstubAllGlobals()
})
const llenarRequeridos = () => {
fireEvent.change(screen.getByLabelText(/^Nombre/), { target: { value: 'Ana Demo' } })
fireEvent.change(screen.getByLabelText(/^Teléfono/), { target: { value: '6641234567' } })
}
describe('AltaAfluenciaForm', () => {
it('sin nombre ni teléfono no llama al API y muestra los errores', () => {
render(<AltaAfluenciaForm onVolver={() => undefined} onPasarAAltaCliente={() => undefined} />)
fireEvent.click(screen.getByRole('button', { name: /Registrar en afluencia/ }))
expect(fetchMock).not.toHaveBeenCalled()
expect(screen.getByText('El nombre es obligatorio')).toBeTruthy()
expect(screen.getByText('El teléfono es obligatorio')).toBeTruthy()
})
it('con email inválido no llama al API', () => {
render(<AltaAfluenciaForm onVolver={() => undefined} onPasarAAltaCliente={() => undefined} />)
llenarRequeridos()
fireEvent.change(screen.getByLabelText(/^Correo/), { target: { value: 'correo-malo' } })
fireEvent.click(screen.getByRole('button', { name: /Registrar en afluencia/ }))
expect(fetchMock).not.toHaveBeenCalled()
expect(screen.getByText('Correo electrónico inválido')).toBeTruthy()
})
it('al registrar (201) muestra el mensaje de éxito', async () => {
fetchMock.mockResolvedValueOnce(respuesta({ id: 'l-1', estado: 'NUEVO', oportunidadId: 'op-1' }))
render(<AltaAfluenciaForm onVolver={() => undefined} onPasarAAltaCliente={() => undefined} />)
llenarRequeridos()
fireEvent.change(screen.getByLabelText(/¿Cómo se enteró de nosotros\?/), { target: { value: 'PASE' } })
fireEvent.change(screen.getByLabelText(/Desarrollo de interés/), { target: { value: 'SENDA' } })
fireEvent.click(screen.getByRole('button', { name: /Registrar en afluencia/ }))
expect((await screen.findByRole('status')).textContent).toContain(
'Registrado en afluencia — se asignará por round-robin',
)
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]
expect(url).toContain('/afluencias')
expect(JSON.parse(String(init.body))).toMatchObject({
nombre: 'Ana Demo',
telefono: '+52 6641234567',
origen: 'PASE',
desarrollo: 'SENDA',
})
})
it('con asignacion "capturista" el mensaje dice que quedó asignado a ti', async () => {
fetchMock.mockResolvedValueOnce(
respuesta({ id: 'l-1', estado: 'NUEVO', oportunidadId: 'op-1', asignacion: 'capturista' }),
)
render(<AltaAfluenciaForm onVolver={() => undefined} onPasarAAltaCliente={() => undefined} />)
llenarRequeridos()
fireEvent.click(screen.getByRole('button', { name: /Registrar en afluencia/ }))
expect((await screen.findByRole('status')).textContent).toContain(
'Registrado en afluencia — asignado a ti',
)
})
it('renderiza los selects de calificación con las opciones de los catálogos', () => {
render(<AltaAfluenciaForm onVolver={() => undefined} onPasarAAltaCliente={() => undefined} />)
const opcionesDe = (label: RegExp) =>
Array.from((screen.getByLabelText(label) as HTMLSelectElement).options).map((o) => o.value)
// El select de modelo arranca deshabilitado hasta elegir desarrollo
const selectModelo = screen.getByLabelText(/Departamento o casa de interés/) as HTMLSelectElement
expect(selectModelo.disabled).toBe(true)
fireEvent.change(screen.getByLabelText(/Desarrollo de interés/), { target: { value: 'SENDA' } })
expect(selectModelo.disabled).toBe(false)
expect(opcionesDe(/Departamento o casa de interés/)).toEqual([
'',
'HIMALAYA',
'SANTIAGO',
'ESMERALDA PA',
'ESMERALDA PB',
'SANTA CRUZ',
])
expect(opcionesDe(/^Estado civil/)).toEqual(['', 'Soltero(a)', 'Casado(a)', 'Unión libre', 'Divorciado(a)', 'Viudo(a)'])
expect(opcionesDe(/Tipo de empleo/)).toEqual(['', 'Asalariado', 'Independiente'])
expect(opcionesDe(/^Crédito/)).toEqual([
'',
'Infonavit',
'Apoyo Infonavit',
'Cofinavit',
'Segundo crédito',
'Contado',
'Fovissste',
'Cofi-Fovissste',
'Bancario',
])
expect(opcionesDe(/Tipo de economía/)).toEqual(['', 'Economía mexicana', 'Economía americana', 'Mixta'])
expect(opcionesDe(/^Califica/)).toEqual(['', 'Sí', 'No', 'Por confirmar'])
})
it('el payload incluye los campos de calificación capturados', async () => {
fetchMock.mockResolvedValueOnce(respuesta({ id: 'l-1', estado: 'NUEVO', oportunidadId: 'op-1' }))
render(<AltaAfluenciaForm onVolver={() => undefined} onPasarAAltaCliente={() => undefined} />)
llenarRequeridos()
fireEvent.change(screen.getByLabelText(/Desarrollo de interés/), { target: { value: 'SENDA' } })
fireEvent.change(screen.getByLabelText(/Departamento o casa de interés/), { target: { value: 'HIMALAYA' } })
fireEvent.change(screen.getByLabelText(/Número de dependientes/), { target: { value: '3' } })
fireEvent.change(screen.getByLabelText(/^Edad/), { target: { value: '35' } })
fireEvent.change(screen.getByLabelText(/^Estado civil/), { target: { value: 'Casado(a)' } })
fireEvent.change(screen.getByLabelText(/Tipo de empleo/), { target: { value: 'Asalariado' } })
fireEvent.change(screen.getByLabelText(/^Ocupación/), { target: { value: 'Ingeniero' } })
fireEvent.change(screen.getByLabelText(/Rango salarial/), { target: { value: '20,000-40,000' } })
fireEvent.change(screen.getByLabelText(/^Crédito/), { target: { value: 'Infonavit' } })
fireEvent.change(screen.getByLabelText(/Tipo de economía/), { target: { value: 'Economía mexicana' } })
fireEvent.change(screen.getByLabelText(/^Califica/), { target: { value: 'Sí' } })
fireEvent.change(screen.getByLabelText(/Lo quiere para/), { target: { value: 'Para vivir' } })
fireEvent.click(screen.getByRole('button', { name: /Registrar en afluencia/ }))
await screen.findByRole('status')
const [, init] = fetchMock.mock.calls[0] as [string, RequestInit]
expect(JSON.parse(String(init.body))).toMatchObject({
modelo: 'HIMALAYA',
numDependientes: '3',
edad: '35',
estadoCivil: 'Casado(a)',
tipoEmpleo: 'Asalariado',
ocupacion: 'Ingeniero',
rangoSalarial: '20,000-40,000',
credito: 'Infonavit',
tipoEconomia: 'Economía mexicana',
califica: 'Sí',
loQuierePara: 'Para vivir',
})
})
it('sin campos de calificación el payload no los incluye', async () => {
fetchMock.mockResolvedValueOnce(respuesta({ id: 'l-1', estado: 'NUEVO', oportunidadId: 'op-1' }))
render(<AltaAfluenciaForm onVolver={() => undefined} onPasarAAltaCliente={() => undefined} />)
llenarRequeridos()
fireEvent.click(screen.getByRole('button', { name: /Registrar en afluencia/ }))
await screen.findByRole('status')
const [, init] = fetchMock.mock.calls[0] as [string, RequestInit]
const cuerpo = JSON.parse(String(init.body)) as Record<string, unknown>
expect(cuerpo).toEqual({ nombre: 'Ana Demo', telefono: '+52 6641234567' })
})
it('con 409 muestra el duplicado con el nombre del lead existente', async () => {
fetchMock.mockResolvedValueOnce(
respuesta({ error: 'Ya existe un lead con ese teléfono', leadId: 'l-9', nombre: 'ANA DEMO' }, 409),
)
render(<AltaAfluenciaForm onVolver={() => undefined} onPasarAAltaCliente={() => undefined} />)
llenarRequeridos()
fireEvent.click(screen.getByRole('button', { name: /Registrar en afluencia/ }))
expect((await screen.findByRole('alert')).textContent).toContain(
'Ya existe un registro con ese teléfono: ANA DEMO.',
)
})
it('con error de red muestra aviso amarillo: el registro se guardó igualmente', async () => {
fetchMock.mockRejectedValueOnce(new Error('red caída'))
render(<AltaAfluenciaForm onVolver={() => undefined} onPasarAAltaCliente={() => undefined} />)
llenarRequeridos()
fireEvent.click(screen.getByRole('button', { name: /Registrar en afluencia/ }))
expect((await screen.findByRole('status')).textContent).toContain(
'La afluencia se registró, pero hubo un problema de conexión. Verifica en Mi día.',
)
})
it('"Capturar otro" limpia el formulario tras el éxito', async () => {
fetchMock.mockResolvedValueOnce(respuesta({ id: 'l-1', estado: 'NUEVO', oportunidadId: 'op-1' }))
render(<AltaAfluenciaForm onVolver={() => undefined} onPasarAAltaCliente={() => undefined} />)
llenarRequeridos()
fireEvent.click(screen.getByRole('button', { name: /Registrar en afluencia/ }))
fireEvent.click(await screen.findByRole('button', { name: /Capturar otro/ }))
expect((screen.getByLabelText(/^Nombre/) as HTMLInputElement).value).toBe('')
expect((screen.getByLabelText(/^Teléfono/) as HTMLInputElement).value).toBe('')
expect(screen.queryByRole('status')).toBeNull()
})
it('"Volver" avisa a la pantalla de entrada', () => {
const onVolver = vi.fn()
render(<AltaAfluenciaForm onVolver={onVolver} onPasarAAltaCliente={() => undefined} />)
fireEvent.click(screen.getByRole('button', { name: 'Volver' }))
expect(onVolver).toHaveBeenCalled()
})
it('tras el éxito, "Pasar a Alta de Cliente" entrega el lead recién creado', async () => {
fetchMock.mockResolvedValueOnce(respuesta({ id: 'op-1', estado: 'Afluencia', oportunidadId: 'op-1' }))
const onPasar = vi.fn()
render(<AltaAfluenciaForm onVolver={() => undefined} onPasarAAltaCliente={onPasar} />)
llenarRequeridos()
fireEvent.change(screen.getByLabelText(/^Correo/), { target: { value: 'ana@correo.com' } })
fireEvent.click(screen.getByRole('button', { name: /Registrar en afluencia/ }))
fireEvent.click(await screen.findByRole('button', { name: /Pasar a Alta de Cliente/ }))
expect(onPasar).toHaveBeenCalledWith({
id: 'op-1',
nombre: 'Ana Demo',
telefono: '+52 6641234567',
email: 'ana@correo.com',
estado: 'Afluencia',
origen: '',
fuente: 'oportunidad', // la captura nueva vive en Oportunidades
})
})
})