ADM-008: Puntos de Venta (CRUD fundacional) #19
30
src/web/src/tests/features/puntos-de-venta/Banners.test.tsx
Normal file
30
src/web/src/tests/features/puntos-de-venta/Banners.test.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { MedioInactivoBanner } from '../../../features/puntos-de-venta/components/MedioInactivoBanner'
|
||||
import { PdvInactivoBanner } from '../../../features/puntos-de-venta/components/PdvInactivoBanner'
|
||||
|
||||
describe('MedioInactivoBanner', () => {
|
||||
it('renders with medio nombre', () => {
|
||||
render(<MedioInactivoBanner medioNombre="Diario El Día" />)
|
||||
expect(screen.getByText(/medio desactivado/i)).toBeInTheDocument()
|
||||
expect(screen.getByText(/diario el día/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders blocked operations message', () => {
|
||||
render(<MedioInactivoBanner medioNombre="Radio AM" />)
|
||||
expect(screen.getByText(/puntos de venta/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('PdvInactivoBanner', () => {
|
||||
it('renders with pdv nombre', () => {
|
||||
render(<PdvInactivoBanner puntoDeVentaNombre="PdV Central" />)
|
||||
expect(screen.getByText(/punto de venta desactivado/i)).toBeInTheDocument()
|
||||
expect(screen.getByText(/pdv central/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders reactivate hint', () => {
|
||||
render(<PdvInactivoBanner puntoDeVentaNombre="PdV Sur" />)
|
||||
expect(screen.getByText(/reactivalo/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,97 @@
|
||||
import { describe, it, expect, beforeAll, afterAll, afterEach, vi } from 'vitest'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { http, HttpResponse } from 'msw'
|
||||
import { setupServer } from 'msw/node'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { MemoryRouter } from 'react-router-dom'
|
||||
import { DeactivatePuntoDeVentaModal } from '../../../features/puntos-de-venta/components/DeactivatePuntoDeVentaModal'
|
||||
|
||||
const API_URL = 'http://localhost:5000'
|
||||
|
||||
vi.mock('sonner', () => ({
|
||||
toast: { success: vi.fn(), error: vi.fn() },
|
||||
}))
|
||||
|
||||
const server = setupServer()
|
||||
|
||||
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
|
||||
afterEach(() => {
|
||||
server.resetHandlers()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
afterAll(() => server.close())
|
||||
|
||||
function renderModal(activo = true) {
|
||||
const qc = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
|
||||
})
|
||||
return render(
|
||||
<QueryClientProvider client={qc}>
|
||||
<MemoryRouter>
|
||||
<DeactivatePuntoDeVentaModal
|
||||
puntoDeVentaId={1}
|
||||
puntoDeVentaNombre="PdV Central"
|
||||
activo={activo}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
</QueryClientProvider>,
|
||||
)
|
||||
}
|
||||
|
||||
describe('DeactivatePuntoDeVentaModal', () => {
|
||||
it('shows "Desactivar" trigger when pdv is active', () => {
|
||||
renderModal(true)
|
||||
expect(screen.getByRole('button', { name: /desactivar/i })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows "Reactivar" trigger when pdv is inactive', () => {
|
||||
renderModal(false)
|
||||
expect(screen.getByRole('button', { name: /reactivar/i })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('opens dialog and shows confirmation text', async () => {
|
||||
renderModal(true)
|
||||
await userEvent.click(screen.getByRole('button', { name: /desactivar/i }))
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText(/desactivar punto de venta/i)).toBeInTheDocument(),
|
||||
)
|
||||
expect(screen.getByText(/pdv central/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('calls deactivate endpoint on confirm', async () => {
|
||||
let called = false
|
||||
server.use(
|
||||
http.post(`${API_URL}/api/v1/admin/puntos-de-venta/1/deactivate`, () => {
|
||||
called = true
|
||||
return new HttpResponse(null, { status: 204 })
|
||||
}),
|
||||
)
|
||||
|
||||
renderModal(true)
|
||||
await userEvent.click(screen.getByRole('button', { name: /desactivar/i }))
|
||||
await waitFor(() => screen.getByRole('alertdialog'))
|
||||
await userEvent.click(screen.getByRole('button', { name: /desactivar$/i }))
|
||||
|
||||
await waitFor(() => expect(called).toBe(true))
|
||||
})
|
||||
|
||||
it('is disabled when disabled prop is true', () => {
|
||||
const qc = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
|
||||
})
|
||||
render(
|
||||
<QueryClientProvider client={qc}>
|
||||
<MemoryRouter>
|
||||
<DeactivatePuntoDeVentaModal
|
||||
puntoDeVentaId={1}
|
||||
puntoDeVentaNombre="PdV Central"
|
||||
activo={true}
|
||||
disabled={true}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
</QueryClientProvider>,
|
||||
)
|
||||
expect(screen.getByRole('button', { name: /desactivar/i })).toBeDisabled()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,145 @@
|
||||
import { describe, it, expect, beforeAll, afterAll, afterEach, vi } from 'vitest'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { http, HttpResponse } from 'msw'
|
||||
import { setupServer } from 'msw/node'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { MemoryRouter } from 'react-router-dom'
|
||||
import { PuntoDeVentaForm } from '../../../features/puntos-de-venta/components/PuntoDeVentaForm'
|
||||
import type { PuntoDeVentaFormValues } from '../../../features/puntos-de-venta/components/PuntoDeVentaForm'
|
||||
import type { PuntoDeVentaDetail } from '../../../features/puntos-de-venta/types'
|
||||
|
||||
const API_URL = 'http://localhost:5000'
|
||||
|
||||
vi.mock('sonner', () => ({
|
||||
toast: { success: vi.fn(), error: vi.fn() },
|
||||
}))
|
||||
|
||||
const mockMedios = [
|
||||
{ id: 1, codigo: 'DIA01', nombre: 'Diario El Día', tipo: 1, plataformaEmpresaId: null, activo: true },
|
||||
{ id: 2, codigo: 'RAD01', nombre: 'Radio AM', tipo: 2, plataformaEmpresaId: null, activo: true },
|
||||
]
|
||||
|
||||
const samplePdv: PuntoDeVentaDetail = {
|
||||
id: 10,
|
||||
medioId: 1,
|
||||
numeroAFIP: 1,
|
||||
nombre: 'PdV Central',
|
||||
activo: true,
|
||||
fechaCreacion: '2026-01-01T00:00:00Z',
|
||||
fechaModificacion: null,
|
||||
}
|
||||
|
||||
const server = setupServer()
|
||||
|
||||
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
|
||||
afterEach(() => {
|
||||
server.resetHandlers()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
afterAll(() => server.close())
|
||||
|
||||
function renderForm(
|
||||
opts: { initialData?: PuntoDeVentaDetail; onSubmit?: (v: PuntoDeVentaFormValues) => void } = {},
|
||||
) {
|
||||
const qc = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
|
||||
})
|
||||
const onSubmit = opts.onSubmit ?? vi.fn()
|
||||
server.use(
|
||||
http.get(`${API_URL}/api/v1/admin/medios`, () =>
|
||||
HttpResponse.json({ items: mockMedios, page: 1, pageSize: 200, total: 2 }),
|
||||
),
|
||||
)
|
||||
render(
|
||||
<QueryClientProvider client={qc}>
|
||||
<MemoryRouter>
|
||||
<PuntoDeVentaForm
|
||||
initialData={opts.initialData}
|
||||
isPending={false}
|
||||
error={null}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
</QueryClientProvider>,
|
||||
)
|
||||
return { onSubmit }
|
||||
}
|
||||
|
||||
describe('PuntoDeVentaForm — create mode', () => {
|
||||
it('shows validation error when nombre is empty', async () => {
|
||||
renderForm()
|
||||
await userEvent.click(screen.getByRole('button', { name: /crear punto de venta/i }))
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText(/nombre es requerido/i)).toBeInTheDocument(),
|
||||
)
|
||||
})
|
||||
|
||||
it('shows validation error when numeroAFIP is 0 or negative', async () => {
|
||||
renderForm()
|
||||
const numeroInput = screen.getByLabelText(/número afip/i)
|
||||
await userEvent.clear(numeroInput)
|
||||
await userEvent.type(numeroInput, '0')
|
||||
await userEvent.click(screen.getByRole('button', { name: /crear punto de venta/i }))
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText(/mayor a 0/i)).toBeInTheDocument(),
|
||||
)
|
||||
})
|
||||
|
||||
it('calls onSubmit with correct values on valid form', async () => {
|
||||
const onSubmit = vi.fn()
|
||||
renderForm({ onSubmit })
|
||||
|
||||
// Select medio
|
||||
const medioTrigger = screen.getByRole('combobox', { name: /medio/i })
|
||||
await userEvent.click(medioTrigger)
|
||||
await waitFor(() =>
|
||||
expect(screen.getByRole('option', { name: 'Diario El Día' })).toBeInTheDocument(),
|
||||
)
|
||||
await userEvent.click(screen.getByRole('option', { name: 'Diario El Día' }))
|
||||
|
||||
// Fill numeroAFIP
|
||||
const numeroInput = screen.getByLabelText(/número afip/i)
|
||||
await userEvent.clear(numeroInput)
|
||||
await userEvent.type(numeroInput, '3')
|
||||
|
||||
// Fill nombre
|
||||
await userEvent.type(screen.getByLabelText(/nombre/i), 'PdV Norte')
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: /crear punto de venta/i }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onSubmit).toHaveBeenCalled()
|
||||
const firstArg = onSubmit.mock.calls[0][0]
|
||||
expect(firstArg).toMatchObject({
|
||||
medioId: 1,
|
||||
numeroAFIP: 3,
|
||||
nombre: 'PdV Norte',
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('PuntoDeVentaForm — edit mode', () => {
|
||||
it('medioId and numeroAFIP are disabled in edit mode', async () => {
|
||||
renderForm({ initialData: samplePdv })
|
||||
const medioTrigger = screen.getByRole('combobox', { name: /medio/i })
|
||||
expect(medioTrigger).toBeDisabled()
|
||||
|
||||
const numeroInput = screen.getByLabelText(/número afip/i) as HTMLInputElement
|
||||
expect(numeroInput.disabled).toBe(true)
|
||||
})
|
||||
|
||||
it('pre-fills form with initialData values', async () => {
|
||||
renderForm({ initialData: samplePdv })
|
||||
await waitFor(() =>
|
||||
expect((screen.getByLabelText(/nombre/i) as HTMLInputElement).value).toBe('PdV Central'),
|
||||
)
|
||||
expect((screen.getByLabelText(/número afip/i) as HTMLInputElement).value).toBe('1')
|
||||
})
|
||||
|
||||
it('shows "Guardar cambios" button in edit mode', () => {
|
||||
renderForm({ initialData: samplePdv })
|
||||
expect(screen.getByRole('button', { name: /guardar cambios/i })).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,164 @@
|
||||
import { describe, it, expect, beforeAll, afterAll, afterEach, vi } from 'vitest'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import { http, HttpResponse } from 'msw'
|
||||
import { setupServer } from 'msw/node'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { MemoryRouter, Routes, Route } from 'react-router-dom'
|
||||
import { PuntosDeVentaListPage } from '../../../features/puntos-de-venta/pages/PuntosDeVentaListPage'
|
||||
import { useAuthStore } from '../../../stores/authStore'
|
||||
|
||||
const API_URL = 'http://localhost:5000'
|
||||
|
||||
vi.mock('sonner', () => ({
|
||||
toast: { success: vi.fn(), error: vi.fn() },
|
||||
}))
|
||||
|
||||
const mockNavigate = vi.fn()
|
||||
vi.mock('react-router-dom', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('react-router-dom')>()
|
||||
return { ...actual, useNavigate: () => mockNavigate }
|
||||
})
|
||||
|
||||
const adminWithPdv = {
|
||||
id: 1,
|
||||
username: 'admin',
|
||||
nombre: 'Admin',
|
||||
rol: 'admin',
|
||||
permisos: ['administracion:puntos_de_venta:gestionar'],
|
||||
mustChangePassword: false,
|
||||
}
|
||||
|
||||
const userWithoutPdv = {
|
||||
id: 2,
|
||||
username: 'cajero',
|
||||
nombre: 'Cajero',
|
||||
rol: 'cajero',
|
||||
permisos: [],
|
||||
mustChangePassword: false,
|
||||
}
|
||||
|
||||
const mockMedios = [
|
||||
{ id: 1, codigo: 'DIA01', nombre: 'Diario El Día', tipo: 1, plataformaEmpresaId: null, activo: true },
|
||||
]
|
||||
|
||||
function makePdvs(n: number) {
|
||||
return Array.from({ length: n }, (_, i) => ({
|
||||
id: i + 1,
|
||||
medioId: 1,
|
||||
numeroAFIP: i + 1,
|
||||
nombre: `PdV ${i + 1}`,
|
||||
activo: true,
|
||||
}))
|
||||
}
|
||||
|
||||
const server = setupServer()
|
||||
|
||||
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
|
||||
afterEach(() => {
|
||||
server.resetHandlers()
|
||||
useAuthStore.getState().clearAuth()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
afterAll(() => server.close())
|
||||
|
||||
function renderPage(user = adminWithPdv) {
|
||||
useAuthStore.setState({ user })
|
||||
const qc = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
|
||||
})
|
||||
return render(
|
||||
<QueryClientProvider client={qc}>
|
||||
<MemoryRouter initialEntries={['/admin/puntos-de-venta']}>
|
||||
<Routes>
|
||||
<Route path="/admin/puntos-de-venta" element={<PuntosDeVentaListPage />} />
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
</QueryClientProvider>,
|
||||
)
|
||||
}
|
||||
|
||||
describe('PuntosDeVentaListPage', () => {
|
||||
it('renders rows when API returns items', async () => {
|
||||
server.use(
|
||||
http.get(`${API_URL}/api/v1/admin/puntos-de-venta`, () =>
|
||||
HttpResponse.json({ items: makePdvs(3), page: 1, pageSize: 20, total: 3 }),
|
||||
),
|
||||
http.get(`${API_URL}/api/v1/admin/medios`, () =>
|
||||
HttpResponse.json({ items: mockMedios, page: 1, pageSize: 200, total: 1 }),
|
||||
),
|
||||
)
|
||||
|
||||
renderPage()
|
||||
|
||||
await waitFor(() => expect(screen.getByText('PdV 1')).toBeInTheDocument())
|
||||
expect(screen.getByText('PdV 2')).toBeInTheDocument()
|
||||
expect(screen.getByText('PdV 3')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows empty state when items is empty', async () => {
|
||||
server.use(
|
||||
http.get(`${API_URL}/api/v1/admin/puntos-de-venta`, () =>
|
||||
HttpResponse.json({ items: [], page: 1, pageSize: 20, total: 0 }),
|
||||
),
|
||||
http.get(`${API_URL}/api/v1/admin/medios`, () =>
|
||||
HttpResponse.json({ items: mockMedios, page: 1, pageSize: 200, total: 1 }),
|
||||
),
|
||||
)
|
||||
|
||||
renderPage()
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText(/sin resultados/i)).toBeInTheDocument(),
|
||||
)
|
||||
})
|
||||
|
||||
it('hides "Nuevo punto de venta" button when user lacks permission', async () => {
|
||||
server.use(
|
||||
http.get(`${API_URL}/api/v1/admin/puntos-de-venta`, () =>
|
||||
HttpResponse.json({ items: [], page: 1, pageSize: 20, total: 0 }),
|
||||
),
|
||||
http.get(`${API_URL}/api/v1/admin/medios`, () =>
|
||||
HttpResponse.json({ items: mockMedios, page: 1, pageSize: 200, total: 1 }),
|
||||
),
|
||||
)
|
||||
|
||||
renderPage(userWithoutPdv)
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.queryByRole('button', { name: /nuevo punto de venta/i })).not.toBeInTheDocument(),
|
||||
)
|
||||
})
|
||||
|
||||
it('shows "Nuevo punto de venta" button when user has permission', async () => {
|
||||
server.use(
|
||||
http.get(`${API_URL}/api/v1/admin/puntos-de-venta`, () =>
|
||||
HttpResponse.json({ items: [], page: 1, pageSize: 20, total: 0 }),
|
||||
),
|
||||
http.get(`${API_URL}/api/v1/admin/medios`, () =>
|
||||
HttpResponse.json({ items: mockMedios, page: 1, pageSize: 200, total: 1 }),
|
||||
),
|
||||
)
|
||||
|
||||
renderPage()
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getByRole('button', { name: /nuevo punto de venta/i })).toBeInTheDocument(),
|
||||
)
|
||||
})
|
||||
|
||||
it('prev button disabled on first page', async () => {
|
||||
server.use(
|
||||
http.get(`${API_URL}/api/v1/admin/puntos-de-venta`, () =>
|
||||
HttpResponse.json({ items: makePdvs(3), page: 1, pageSize: 20, total: 3 }),
|
||||
),
|
||||
http.get(`${API_URL}/api/v1/admin/medios`, () =>
|
||||
HttpResponse.json({ items: mockMedios, page: 1, pageSize: 200, total: 1 }),
|
||||
),
|
||||
)
|
||||
|
||||
renderPage()
|
||||
|
||||
await waitFor(() => expect(screen.getByText('PdV 1')).toBeInTheDocument())
|
||||
expect(screen.getByRole('button', { name: /anterior/i })).toBeDisabled()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user