feat(mejoras): 25 mejoras UX en todos los módulos + purga Monday
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
This commit is contained in:
91
apps/cotizador/src/pages/CotizadorPage.smoke.test.tsx
Normal file
91
apps/cotizador/src/pages/CotizadorPage.smoke.test.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
// @vitest-environment jsdom
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { MemoryRouter } from 'react-router-dom'
|
||||
import CotizadorPage from './CotizadorPage'
|
||||
import { INVENTARIO_SEED } from '../data/unidades'
|
||||
|
||||
/**
|
||||
* Smoke del cierre de la cotización:
|
||||
* - barra sticky con propuesta elegida + Compartir/Guardar/Generar PDF
|
||||
* - preselección por defecto de la propuesta de menor total
|
||||
* - sin servidor: la cotización se guarda localmente con folio CTE-LOCAL-*
|
||||
*/
|
||||
const unidad = INVENTARIO_SEED.find((u) => u.status === 'DISPONIBLE' && u.desarrollo === 'DIAGONAL')!
|
||||
|
||||
const respuesta = (data: unknown, status = 200) =>
|
||||
new Response(JSON.stringify(data), { status, headers: { 'Content-Type': 'application/json' } })
|
||||
|
||||
// El entorno de test no trae localStorage: mock en memoria para el fallback local
|
||||
const almacenamiento = new Map<string, string>()
|
||||
beforeEach(() => {
|
||||
almacenamiento.clear()
|
||||
vi.stubGlobal('localStorage', {
|
||||
getItem: (k: string) => almacenamiento.get(k) ?? null,
|
||||
setItem: (k: string, v: string) => void almacenamiento.set(k, v),
|
||||
removeItem: (k: string) => void almacenamiento.delete(k),
|
||||
clear: () => almacenamiento.clear(),
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
describe('CotizadorPage — barra sticky y guardado local', () => {
|
||||
it('preselecciona la propuesta más barata y guarda local con folio CTE-LOCAL cuando el API no responde', async () => {
|
||||
const fetchMock = vi.fn((input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const url = String(input)
|
||||
const metodo = init?.method ?? 'GET'
|
||||
if (url.includes('/unidades')) return Promise.resolve(respuesta([unidad]))
|
||||
if (url.includes('/cotizaciones/mias')) return Promise.resolve(respuesta([]))
|
||||
if (url.includes('/cotizaciones') && metodo === 'POST') {
|
||||
return Promise.reject(new TypeError('servidor caído'))
|
||||
}
|
||||
return Promise.reject(new TypeError(`fetch inesperado: ${metodo} ${url}`))
|
||||
})
|
||||
vi.stubGlobal('fetch', fetchMock)
|
||||
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<CotizadorPage />
|
||||
</MemoryRouter>,
|
||||
)
|
||||
|
||||
// Barra sticky siempre presente, con las tres acciones
|
||||
expect(await screen.findByRole('button', { name: 'Generar PDF' })).toBeTruthy()
|
||||
expect(screen.getByRole('button', { name: 'Guardar' })).toBeTruthy()
|
||||
expect(screen.getByRole('button', { name: 'Compartir' })).toBeTruthy()
|
||||
expect(screen.getByText(/Selecciona una unidad para elegir propuesta/)).toBeTruthy()
|
||||
|
||||
// Elegir una unidad del inventario
|
||||
const inputUnidad = screen.getByLabelText('Unidad') as HTMLInputElement
|
||||
fireEvent.focus(inputUnidad)
|
||||
fireEvent.change(inputUnidad, { target: { value: unidad.codigo } })
|
||||
fireEvent.mouseDown(screen.getByRole('option', { name: new RegExp(unidad.codigo) }))
|
||||
|
||||
// Preselección por defecto: la propuesta de menor total pagado
|
||||
const selectPropuesta = (await screen.findByLabelText(
|
||||
'Cambiar propuesta elegida',
|
||||
)) as HTMLSelectElement
|
||||
await waitFor(() => expect(selectPropuesta.value).toBe('2')) // Contado es la más barata
|
||||
expect(screen.getByText(/Propuesta elegida: #3/)).toBeTruthy()
|
||||
|
||||
// Guardar con el servidor caído → folio local persistido en el dispositivo
|
||||
fireEvent.change(screen.getByLabelText(/Nombre del cliente/), {
|
||||
target: { value: 'Cliente Prueba Local' },
|
||||
})
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Guardar' }))
|
||||
|
||||
await waitFor(() => {
|
||||
const locales = JSON.parse(
|
||||
almacenamiento.get('frasa-cotizador:cotizaciones-locales') ?? '[]',
|
||||
) as { folio?: string; clienteNombre?: string }[]
|
||||
expect(locales).toHaveLength(1)
|
||||
expect(locales[0]?.folio ?? '').toMatch(/^CTE-LOCAL-\d+$/)
|
||||
expect(locales[0]?.clienteNombre).toBe('Cliente Prueba Local')
|
||||
})
|
||||
expect(screen.getByText(/cotización guardada localmente con folio CTE-LOCAL-/)).toBeTruthy()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user