Backend: mi-dia siempre local, vocabulario canónico, AplicacionPago con usuario Mi día: modal español real, cita desde modal, score falso eliminado Cotizador: barra sticky, persistencia local con folio Alta Clientes: redirect eliminado, foto conectada, asesor desde sesión, refs dinámicas Cobranza: contrato Mora arreglado, link a Plan, abono próximo pago, WhatsApp Landing: contactos locales, ModalContacto eliminado, ModoTrabajo eliminado Dashboards: MiTablero unificado, funnel con descartados, drill-down, rango fechas Chat FRASA: corpus 5 desarrollos, 6 herramientas, historial conversaciones 69 archivos, 7 apps construidas
157 lines
7.4 KiB
TypeScript
157 lines
7.4 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
import AltaClientePage from './AltaClientePage'
|
|
|
|
// El lienzo de firma necesita canvas 2d (no existe en jsdom); el paso 7 se
|
|
// prueba solo hasta la orquestación de la página, no el trazo de la firma.
|
|
vi.mock('../forms/FirmasForm', () => ({ default: () => <div>Paso de firmas</div> }))
|
|
|
|
type FetchFn = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>
|
|
|
|
const respuesta = (data: unknown, status = 200) =>
|
|
new Response(JSON.stringify(data), { status, headers: { 'Content-Type': 'application/json' } })
|
|
|
|
let fetchMock: ReturnType<typeof vi.fn<FetchFn>>
|
|
|
|
const llamadasA = (fragmento: string, metodo?: string) =>
|
|
fetchMock.mock.calls.filter(
|
|
([input, init]) => String(input).includes(fragmento) && (metodo === undefined || (init?.method ?? 'GET') === metodo),
|
|
)
|
|
|
|
beforeEach(() => {
|
|
fetchMock = vi.fn<FetchFn>().mockImplementation((input, init) => {
|
|
const url = String(input)
|
|
const metodo = init?.method ?? 'GET'
|
|
if (url.includes('/borradores') && metodo === 'GET') return Promise.resolve(respuesta([]))
|
|
if (url.includes('/borradores') && metodo === 'POST') {
|
|
return Promise.resolve(respuesta({ id: 'b-1', nombre: 'B', paso: 1, datosJson: '{}' }), )
|
|
}
|
|
if (url.includes('/borradores') && metodo === 'DELETE') return Promise.resolve(respuesta({}))
|
|
if (url.endsWith('/clientes/progreso/iniciar')) {
|
|
return Promise.resolve(respuesta({ id: 'prog-1', mondayContactoId: 'cont-1' }, 201))
|
|
}
|
|
if (url.includes('/clientes/progreso/prog-1/completar')) return Promise.resolve(respuesta({ id: 'prog-1' }, 201))
|
|
if (url.includes('/clientes/progreso/prog-1') && metodo === 'PATCH') {
|
|
return Promise.resolve(respuesta({ id: 'prog-1', mondaySync: true }))
|
|
}
|
|
if (url.endsWith('/clientes') && metodo === 'POST') return Promise.resolve(respuesta({ id: 'cli-directo' }, 201))
|
|
return Promise.reject(new Error(`fetch inesperado: ${metodo} ${url}`))
|
|
})
|
|
vi.stubGlobal('fetch', fetchMock)
|
|
})
|
|
|
|
afterEach(() => {
|
|
cleanup()
|
|
vi.unstubAllGlobals()
|
|
})
|
|
|
|
const entrarAlWizard = async () => {
|
|
render(<AltaClientePage />)
|
|
fireEvent.click(await screen.findByRole('button', { name: /Alta de Cliente/ }))
|
|
fireEvent.click(await screen.findByRole('button', { name: 'Empezar nuevo' }))
|
|
}
|
|
|
|
const llenarPaso0 = async () => {
|
|
fireEvent.change(await screen.findByLabelText(/Nombre completo/), { target: { value: 'Ana Demo Progreso' } })
|
|
fireEvent.change(screen.getByLabelText(/^CURP/), { target: { value: 'AEDA900101MBCXXX00' } })
|
|
fireEvent.change(screen.getByLabelText(/^RFC/), { target: { value: 'AEDA900101XX0' } })
|
|
fireEvent.change(screen.getByLabelText(/^Email/), { target: { value: 'ana@correo.com' } })
|
|
fireEvent.change(screen.getByLabelText(/Celular/), { target: { value: '6649990003' } })
|
|
fireEvent.change(screen.getByLabelText(/Fecha de nacimiento/), { target: { value: '1990-01-01' } })
|
|
}
|
|
|
|
const avanzar = async () => {
|
|
fireEvent.click(screen.getByRole('button', { name: 'Siguiente' }))
|
|
await act(async () => {})
|
|
}
|
|
|
|
describe('AltaClientePage — guardado progresivo (etapa por etapa)', () => {
|
|
it('al avanzar del paso 0 con nombre inicia el progreso (Contacto PROSPECTO)', async () => {
|
|
await entrarAlWizard()
|
|
await llenarPaso0()
|
|
await avanzar()
|
|
|
|
// Avanzó al paso Empleo y subió el inicio del progreso con datosGenerales
|
|
expect(await screen.findByLabelText(/Tipo de empleo/)).toBeTruthy()
|
|
await waitFor(() => expect(llamadasA('/clientes/progreso/iniciar', 'POST')).toHaveLength(1))
|
|
const cuerpo = JSON.parse(String(llamadasA('/clientes/progreso/iniciar', 'POST')[0]?.[1]?.body)) as {
|
|
datosGenerales: { nombreCompleto: string }
|
|
}
|
|
expect(cuerpo.datosGenerales.nombreCompleto).toBe('Ana Demo Progreso')
|
|
})
|
|
|
|
it('los pasos siguientes hacen PATCH del progreso ya iniciado', async () => {
|
|
await entrarAlWizard()
|
|
await llenarPaso0()
|
|
await avanzar()
|
|
await screen.findByLabelText(/Tipo de empleo/)
|
|
await waitFor(() => expect(llamadasA('/clientes/progreso/iniciar', 'POST')).toHaveLength(1))
|
|
|
|
await avanzar() // Empleo → Económicos
|
|
await waitFor(() => expect(llamadasA('/clientes/progreso/prog-1', 'PATCH')).toHaveLength(1))
|
|
// No se duplica el inicio
|
|
expect(llamadasA('/clientes/progreso/iniciar', 'POST')).toHaveLength(1)
|
|
})
|
|
|
|
it('si el sync falla, el avance no se bloquea y solo queda un aviso', async () => {
|
|
fetchMock.mockImplementation((input, init) => {
|
|
const url = String(input)
|
|
const metodo = init?.method ?? 'GET'
|
|
if (url.includes('/borradores') && metodo === 'GET') return Promise.resolve(respuesta([]))
|
|
if (url.includes('/borradores') && metodo === 'POST') {
|
|
return Promise.resolve(respuesta({ id: 'b-1', nombre: 'B', paso: 1, datosJson: '{}' }))
|
|
}
|
|
if (url.endsWith('/clientes/progreso/iniciar')) return Promise.reject(new Error('API caída'))
|
|
return Promise.reject(new Error(`fetch inesperado: ${metodo} ${url}`))
|
|
})
|
|
|
|
await entrarAlWizard()
|
|
await llenarPaso0()
|
|
await avanzar()
|
|
|
|
// Avanzó igual al paso Empleo y solo queda el aviso sutil
|
|
expect(await screen.findByLabelText(/Tipo de empleo/)).toBeTruthy()
|
|
await waitFor(() => {
|
|
const avisos = screen.getAllByRole('status').map((el) => el.textContent ?? '')
|
|
expect(avisos.some((a) => a.includes('El avance quedó guardado local'))).toBe(true)
|
|
})
|
|
})
|
|
|
|
it('el submit final usa el endpoint progresivo cuando hay progresoId', async () => {
|
|
await entrarAlWizard()
|
|
await llenarPaso0()
|
|
await avanzar()
|
|
await screen.findByLabelText(/Tipo de empleo/)
|
|
await waitFor(() => expect(llamadasA('/clientes/progreso/iniciar', 'POST')).toHaveLength(1))
|
|
|
|
await avanzar() // Empleo → Económicos
|
|
await screen.findByLabelText(/Ingresos mensuales/)
|
|
await avanzar() // Económicos → Co-acreditado
|
|
await screen.findByLabelText(/co-acreditado o cónyuge/)
|
|
await avanzar() // Co-acreditado → Referencias
|
|
// Referencias exige al menos 1 con nombre y teléfono
|
|
fireEvent.change((await screen.findAllByLabelText(/^Nombre completo/))[0]!, { target: { value: 'Ref Uno' } })
|
|
fireEvent.change(screen.getAllByLabelText(/^Teléfono/)[0]!, { target: { value: '6641112233' } })
|
|
await avanzar() // Referencias → Vivienda y crédito
|
|
fireEvent.change(await screen.findByLabelText(/^Proyecto/), { target: { value: 'DIAGONAL' } })
|
|
await avanzar() // Vivienda → Documentos
|
|
|
|
// Documentos → Firmas: dispara el submit final del alta
|
|
await avanzar()
|
|
await waitFor(() => expect(llamadasA('/clientes/progreso/prog-1/completar', 'POST')).toHaveLength(1))
|
|
// NO se usó el endpoint legado
|
|
expect(
|
|
fetchMock.mock.calls.filter(
|
|
([input, init]) => String(input).endsWith('/clientes') && (init?.method ?? '') === 'POST',
|
|
),
|
|
).toHaveLength(0)
|
|
// El flujo posterior queda idéntico: alta registrada con el id del progreso
|
|
expect(await screen.findByText(/Alta registrada correctamente/)).toBeTruthy()
|
|
expect(screen.getByText(/Folio prog-1/)).toBeTruthy()
|
|
// Pantalla de éxito con las acciones post-registro (sin redirect automático)
|
|
expect(screen.getByRole('button', { name: 'Generar liga de firma' })).toBeTruthy()
|
|
expect(screen.getByRole('button', { name: 'Ir a Mi día' })).toBeTruthy()
|
|
expect(screen.getByRole('button', { name: 'Registrar otro cliente' })).toBeTruthy()
|
|
})
|
|
})
|