API: - GET /api/monday/usuarios: usuarios activos no-guest del workspace - PATCH /api/usuarios/me/monday: vincula/desvincula el mondayUserId del usuario autenticado (valida contra el workspace) - GET /api/leads/buscar?q=: búsqueda de leads por nombre/teléfono/email (sin acentos, orden updatedAt desc, límite 15) - aPublico/me ahora incluyen mondayUserId Landing (/): - Nueva sección 'Configuración de cuenta': login inline, selector del usuario de Monday, guardar/desvincular, cerrar sesión Alta de clientes (/client): - Búsqueda de leads de Monday con debounce en la pantalla de entrada; al elegir uno prellena nombre/email/celular y guarda leadMondayId - El picker (borradores + alta nueva + búsqueda de lead) es ahora siempre la pantalla de entrada Tests: api 294, landing 8, alta-clientes 96 — todos verdes
135 lines
5.2 KiB
TypeScript
135 lines
5.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 { ConfiguracionCuenta } from './ConfiguracionCuenta'
|
|
|
|
type FetchFn = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>
|
|
|
|
/** Stub sencillo de localStorage (jsdom no lo expone como global con vitest 2) */
|
|
function crearStorageStub(): Storage {
|
|
const mapa = new Map<string, string>()
|
|
return {
|
|
getItem: (k: string) => mapa.get(k) ?? null,
|
|
setItem: (k: string, v: string) => void mapa.set(k, v),
|
|
removeItem: (k: string) => void mapa.delete(k),
|
|
clear: () => mapa.clear(),
|
|
key: () => null,
|
|
get length() {
|
|
return mapa.size
|
|
},
|
|
} as Storage
|
|
}
|
|
|
|
const USUARIOS_MONDAY = [
|
|
{ id: 13676, nombre: 'Erick Peña', email: 'erick@frasa.mx' },
|
|
{ id: 20400, nombre: 'Cristina Barrera', email: 'cristina@frasa.mx' },
|
|
]
|
|
|
|
const sesionFake = {
|
|
token: 'jwt-de-prueba',
|
|
usuario: { nombre: 'ERICK ISRAEL PEÑA RAMOS', rol: 'vendedor', asignacion: 'MONTEBELLO', mondayUserId: null },
|
|
}
|
|
|
|
const respuestaOk = (data: unknown) =>
|
|
new Response(JSON.stringify(data), { status: 200, headers: { 'Content-Type': 'application/json' } })
|
|
|
|
let fetchMock: ReturnType<typeof vi.fn<FetchFn>>
|
|
|
|
beforeEach(() => {
|
|
fetchMock = vi.fn<FetchFn>()
|
|
vi.stubGlobal('fetch', fetchMock)
|
|
vi.stubGlobal('localStorage', crearStorageStub())
|
|
})
|
|
|
|
afterEach(() => {
|
|
cleanup()
|
|
vi.unstubAllGlobals()
|
|
})
|
|
|
|
describe('ConfiguracionCuenta sin sesión', () => {
|
|
it('muestra el formulario de login y no llama al API', () => {
|
|
render(<ConfiguracionCuenta />)
|
|
expect(screen.getByRole('heading', { name: 'Configuración de cuenta' })).toBeTruthy()
|
|
expect(screen.getByLabelText('Usuario')).toBeTruthy()
|
|
expect(screen.getByLabelText('Contraseña')).toBeTruthy()
|
|
expect(fetchMock).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('login fallido muestra el mensaje de error', async () => {
|
|
fetchMock.mockResolvedValue(new Response('{}', { status: 401 }))
|
|
render(<ConfiguracionCuenta />)
|
|
|
|
fireEvent.change(screen.getByLabelText('Usuario'), { target: { value: 'nadie' } })
|
|
fireEvent.change(screen.getByLabelText('Contraseña'), { target: { value: 'mala1' } })
|
|
fireEvent.click(screen.getByRole('button', { name: 'Entrar' }))
|
|
|
|
expect((await screen.findByRole('alert')).textContent).toContain('Usuario o contraseña incorrectos')
|
|
})
|
|
})
|
|
|
|
describe('ConfiguracionCuenta con sesión', () => {
|
|
beforeEach(() => {
|
|
localStorage.setItem('frasa_auth', JSON.stringify(sesionFake))
|
|
})
|
|
|
|
it('muestra el perfil, el dropdown de usuarios Monday y guarda el vínculo con PATCH', async () => {
|
|
fetchMock.mockImplementation((input, init) => {
|
|
const url = String(input)
|
|
if (url.endsWith('/auth/me')) return Promise.resolve(respuestaOk(sesionFake.usuario))
|
|
if (url.endsWith('/monday/usuarios')) return Promise.resolve(respuestaOk(USUARIOS_MONDAY))
|
|
if (url.endsWith('/usuarios/me/monday') && init?.method === 'PATCH') {
|
|
return Promise.resolve(respuestaOk({ ...sesionFake.usuario, mondayUserId: 13676 }))
|
|
}
|
|
return Promise.reject(new Error(`fetch inesperado: ${url}`))
|
|
})
|
|
|
|
render(<ConfiguracionCuenta />)
|
|
|
|
// Perfil y dropdown cargados
|
|
expect(await screen.findByText('ERICK ISRAEL PEÑA RAMOS')).toBeTruthy()
|
|
const dropdown = await screen.findByLabelText('Tu usuario de Monday')
|
|
expect(screen.getByText('sin vincular')).toBeTruthy()
|
|
|
|
fireEvent.change(dropdown, { target: { value: '13676' } })
|
|
fireEvent.click(screen.getByRole('button', { name: 'Guardar' }))
|
|
|
|
await waitFor(() => {
|
|
const llamadaPatch = fetchMock.mock.calls.find(
|
|
([url, init]) => String(url).endsWith('/usuarios/me/monday') && init?.method === 'PATCH',
|
|
)
|
|
expect(llamadaPatch).toBeTruthy()
|
|
expect(JSON.parse(String(llamadaPatch?.[1]?.body))).toEqual({ mondayUserId: 13676 })
|
|
})
|
|
expect((await screen.findByRole('status')).textContent).toContain('Vínculo con Monday guardado')
|
|
// El PATCH fue con Bearer
|
|
const [, initPatch] = fetchMock.mock.calls.find(
|
|
([url, init]) => String(url).endsWith('/usuarios/me/monday') && init?.method === 'PATCH',
|
|
) as [string, RequestInit]
|
|
expect((initPatch.headers as Record<string, string>)['Authorization']).toBe('Bearer jwt-de-prueba')
|
|
})
|
|
|
|
it('si falla /monday/usuarios muestra error reintentable y el reintento carga la lista', async () => {
|
|
let intentos = 0
|
|
fetchMock.mockImplementation((input) => {
|
|
const url = String(input)
|
|
if (url.endsWith('/auth/me')) return Promise.resolve(respuestaOk(sesionFake.usuario))
|
|
if (url.endsWith('/monday/usuarios')) {
|
|
intentos += 1
|
|
return intentos === 1
|
|
? Promise.resolve(new Response('{}', { status: 500 }))
|
|
: Promise.resolve(respuestaOk(USUARIOS_MONDAY))
|
|
}
|
|
return Promise.reject(new Error(`fetch inesperado: ${url}`))
|
|
})
|
|
|
|
render(<ConfiguracionCuenta />)
|
|
|
|
expect(
|
|
await screen.findByText('No se pudo cargar la lista de usuarios de Monday.'),
|
|
).toBeTruthy()
|
|
fireEvent.click(screen.getByRole('button', { name: 'Reintentar' }))
|
|
expect(await screen.findByLabelText('Tu usuario de Monday')).toBeTruthy()
|
|
expect(intentos).toBe(2)
|
|
})
|
|
})
|