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 const respuesta = (data: unknown, status = 201) => new Response(JSON.stringify(data), { status, headers: { 'Content-Type': 'application/json' } }) let fetchMock: ReturnType> beforeEach(() => { fetchMock = vi.fn() 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( 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( 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( undefined} onPasarAAltaCliente={() => undefined} />) llenarRequeridos() fireEvent.change(screen.getByLabelText(/¿Cómo se enteró de nosotros\?/), { target: { value: 'Web' } }) 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('/leads') expect(JSON.parse(String(init.body))).toMatchObject({ nombre: 'Ana Demo', telefono: '6641234567', origen: 'Web', 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( 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( 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( 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(/Medio de contacto/), { target: { value: 'Llamada' } }) 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 - $30,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: 'Vivir ahí' } }) 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', medioContacto: 'Llamada', edad: '35', estadoCivil: 'Casado(a)', tipoEmpleo: 'Asalariado', ocupacion: 'Ingeniero', rangoSalarial: '$20,000 - $30,000', credito: 'Infonavit', tipoEconomia: 'Economía mexicana', califica: 'Sí', loQuierePara: 'Vivir ahí', }) }) it('sin campos de calificación el payload no los incluye', async () => { fetchMock.mockResolvedValueOnce(respuesta({ id: 'l-1', estado: 'NUEVO', oportunidadId: 'op-1' })) render( 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 expect(cuerpo).toEqual({ nombre: 'Ana Demo', telefono: '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( undefined} onPasarAAltaCliente={() => undefined} />) llenarRequeridos() fireEvent.click(screen.getByRole('button', { name: /Registrar en afluencia/ })) expect((await screen.findByRole('alert')).textContent).toContain( 'Ya existe un lead con ese teléfono: ANA DEMO.', ) }) it('con error de red muestra un mensaje reintentable', async () => { fetchMock.mockRejectedValueOnce(new Error('red caída')) render( undefined} onPasarAAltaCliente={() => undefined} />) llenarRequeridos() fireEvent.click(screen.getByRole('button', { name: /Registrar en afluencia/ })) expect((await screen.findByRole('alert')).textContent).toContain( 'No se pudo registrar el lead. Revisa tu conexión e intenta de nuevo.', ) }) it('"Capturar otro" limpia el formulario tras el éxito', async () => { fetchMock.mockResolvedValueOnce(respuesta({ id: 'l-1', estado: 'NUEVO', oportunidadId: 'op-1' })) render( 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( 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: 'l-1', estado: 'NUEVO', oportunidadId: 'op-1' })) const onPasar = vi.fn() render( 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: 'l-1', nombre: 'Ana Demo', telefono: '6641234567', email: 'ana@correo.com', estado: 'NUEVO', origen: '', }) }) })