feat(alta): separar en Alta Afluencia y Alta de Cliente
- Nueva pantalla de entrada en /client con dos opciones:
· Alta Afluencia: captura ligera (nombre, teléfono, email, origen,
desarrollo de interés, notas) → POST /api/leads (lead + oportunidad
Afluencia + round-robin, todo ya existente en el API)
· Alta de Cliente: formato completo actual (picker + wizard intactos)
- Manejo de duplicado por teléfono (409 con nombre del lead existente)
- ?cotizacion=<id> sigue saltando directo al wizard
- Tests: 107 verdes (11 nuevos); smoke en vivo OK y limpio
This commit is contained in:
111
apps/alta-clientes/src/components/AltaAfluenciaForm.test.tsx
Normal file
111
apps/alta-clientes/src/components/AltaAfluenciaForm.test.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
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} />)
|
||||
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} />)
|
||||
llenarRequeridos()
|
||||
fireEvent.change(screen.getByLabelText(/^Email/), { 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} />)
|
||||
llenarRequeridos()
|
||||
fireEvent.change(screen.getByLabelText(/^Origen/), { 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 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} />)
|
||||
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(<AltaAfluenciaForm onVolver={() => 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(<AltaAfluenciaForm onVolver={() => 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} />)
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Volver' }))
|
||||
expect(onVolver).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user