// @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() 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( , ) // 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( , ) // 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) } }) })