165 lines
7.0 KiB
TypeScript
165 lines
7.0 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'
|
|
import { mensualidadesParaUnidad } from '../hooks/useCotizacion'
|
|
|
|
/**
|
|
* Smoke del cierre de la cotización con el esquema único FRASA 2026:
|
|
* - barra sticky con resumen del esquema + Compartir/Guardar/Generar PDF
|
|
* - panel único (línea de tiempo Hoy → Firma → Mensualidades)
|
|
* - 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('muestra el esquema único 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 ver el esquema de cotización\./)).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) }))
|
|
|
|
// Panel único del esquema: línea de tiempo y conceptos
|
|
expect(await screen.findByText('Esquema FRASA 2026 — conceptos')).toBeTruthy()
|
|
expect(screen.getByText('Hoy')).toBeTruthy()
|
|
expect(screen.getByText('A la firma')).toBeTruthy()
|
|
// El tope de mensualidades son los meses que faltan para la entrega
|
|
expect(screen.getByRole('option', { name: `${mensualidadesParaUnidad(unidad)} mensualidades` })).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
|
|
esquema?: string
|
|
tipoPropuesta?: string
|
|
saldoFirma?: number | null
|
|
}[]
|
|
expect(locales).toHaveLength(1)
|
|
expect(locales[0]?.folio ?? '').toMatch(/^CTE-LOCAL-\d+$/)
|
|
expect(locales[0]?.clienteNombre).toBe('Cliente Prueba Local')
|
|
// La instantánea local ya vive en el esquema único
|
|
expect(locales[0]?.esquema).toBe('FRASA_2026')
|
|
expect(locales[0]?.tipoPropuesta).toBe('FRASA_2026')
|
|
expect(locales[0]?.saldoFirma).not.toBeNull()
|
|
})
|
|
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,
|
|
esquema: 'FRASA_2026',
|
|
tipoPropuesta: 'BANCARIO',
|
|
acabados: false,
|
|
apartado: 20000,
|
|
descuento: 0,
|
|
precioLista: vendida.precioConAcabados,
|
|
precioFinal: 0,
|
|
saldoFirma: null,
|
|
engancheDiferible: null,
|
|
escrituracion: null,
|
|
escrituracionDiferida: false,
|
|
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()
|
|
// Es una cotización del esquema anterior (legacy): lo dice al retomarla
|
|
expect(screen.getByText(/Usa el esquema anterior \(BANCARIO\)/)).toBeTruthy()
|
|
for (const nombre of ['Generar PDF', 'Guardar', 'Compartir']) {
|
|
expect((screen.getByRole('button', { name: nombre }) as HTMLButtonElement).disabled).toBe(true)
|
|
}
|
|
})
|
|
})
|