Files
FRASA-CAS/apps/cotizador/src/pages/CotizadorPage.smoke.test.tsx

147 lines
6.2 KiB
TypeScript

// @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()
window.history.replaceState({}, '', '/')
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()
})
it('no deja cotizar una unidad que ya no está disponible: avisa y bloquea las acciones', async () => {
const vendida = { ...unidad, codigo: 'U-900', status: 'VENDIDO' as const }
const cotizacionVendida = {
id: 'cot-vendida',
folio: 'COT-VENDID',
unidadCodigo: vendida.codigo,
desarrollo: 'DIAGONAL',
clienteNombre: 'Cliente Antiguo',
clienteTelefono: null,
clienteEmail: null,
tipoPropuesta: 'BANCARIO',
acabados: false,
apartado: 20000,
descuento: 0,
precioLista: vendida.precioConAcabados,
precioFinal: 0,
enganche: 0,
numMensualidades: 12,
mensualidad: 1000,
pagoFinal: 0,
createdAt: '2026-09-01T00:00:00.000Z',
}
const fetchMock = vi.fn((input: RequestInfo | URL) => {
const url = String(input)
if (url.includes('/unidades')) return Promise.resolve(respuesta([unidad, vendida]))
if (url.includes('/cotizaciones/mias')) return Promise.resolve(respuesta([cotizacionVendida]))
return Promise.reject(new TypeError(`fetch inesperado: ${url}`))
})
vi.stubGlobal('fetch', fetchMock)
// Prellenado desde Mi día con una unidad que ya se vendió
window.history.replaceState({}, '', `/?unidad=${vendida.codigo}`)
render(
<MemoryRouter>
<CotizadorPage />
</MemoryRouter>,
)
// El prellenado NO la preselecciona y lo dice
expect(await screen.findByText(/ya no está disponible \(estatus: VENDIDO\)/)).toBeTruthy()
// Retomar la cotización vieja de esa unidad: se restaura, pero avisa y
// deja Guardar/Compartir/Generar PDF bloqueados
fireEvent.click(await screen.findByRole('button', { name: /Ver historial/ }))
fireEvent.click(await screen.findByRole('button', { name: 'Retomar' }))
const alerta = await screen.findByRole('alert')
expect(alerta.textContent).toContain(`La unidad ${vendida.codigo} no está disponible`)
expect(screen.getByText(/ya no está disponible \(estatus: VENDIDO\): no se puede cotizar de nuevo/)).toBeTruthy()
for (const nombre of ['Generar PDF', 'Guardar', 'Compartir']) {
expect((screen.getByRole('button', { name: nombre }) as HTMLButtonElement).disabled).toBe(true)
}
})
})