// @vitest-environment happy-dom // // DocumentPage: dos badges independientes con texto explícito, 1 primaria + // secundarias visibles, destructiva separada y bloqueada con motivo, botones // inteligentes con contador y pestaña «records» desde una fuente. import { afterEach, describe, expect, it, vi } from 'vitest' import { cleanup, render, screen, fireEvent, waitFor } from '@testing-library/react' vi.mock('react-i18next', () => ({ useTranslation: () => ({ t: (k: string, o?: any) => o?.defaultValue ?? k }), })) vi.mock('../../dynamic-icon', () => ({ DynamicIcon: () => null })) import { DocumentPage } from '../document-page' import { ApiProvider, type ApiClient } from '../../api-context' import type { DocumentPageRegistration } from '../registry' afterEach(cleanup) const registration: DocumentPageRegistration = { spec: { model: 'invoices', title: '{{number}}', statuses: [ { id: 'fiscal', label: 'Estado fiscal', field: '$.fiscal', states: [{ value: 'stamped', label: 'Timbrada', tone: 'success' }], }, { id: 'billing', label: 'Estado de cobro', field: '$.billing', states: [{ value: 'partial', label: 'Parcial', tone: 'caution' }], }, ], metrics: [{ label: 'Total', path: 'total', format: 'money' }], actions: [ { key: 'pay', label: 'Registrar pago' }, { key: 'send', label: 'Enviar' }, { key: 'pdf', label: 'Descargar PDF', openUrl: '{{$.pdf}}' }, { key: 'cancel', label: 'Solicitar cancelación', destructive: true, blockedWhen: { when: { field: 'sources.reps.length', op: 'gt', value: 0 }, reason: 'Cancela primero los REP' }, }, ], layouts: [{ when: { field: '$.fiscal', op: 'eq', value: 'stamped' }, layout: { primary: 'pay', secondary: ['send', 'pdf'], destructive: ['cancel'] } }], smartButtons: [{ key: 'reps', label: 'REP', source: 'reps', tab: 'reps' }], sources: [{ key: 'reps', model: 'fiscal_documents', foreignKey: 'source_id' }], tabs: [ { key: 'reps', label: 'Pagos y REP', kind: 'records', source: 'reps', columns: [{ label: 'UUID', path: 'fiscal_uuid' }] }, ], }, derive: () => ({ fiscal: 'stamped', billing: 'partial', pdf: 'https://x/y.pdf' }), } function setup(onAction = vi.fn()) { const api = { get: vi.fn().mockResolvedValue({ data: { success: true, data: [{ id: 'r1', fiscal_uuid: 'REP-UUID-1' }] } }), } as unknown as ApiClient render( , ) return { api, onAction } } describe('DocumentPage', () => { it('muestra cabecera con dos badges de texto explícito y el total en la moneda de la org', async () => { setup() expect(screen.getByRole('heading', { name: 'A-42' })).toBeTruthy() expect(screen.getByText('Timbrada')).toBeTruthy() expect(screen.getByText('Parcial')).toBeTruthy() expect(screen.getByText(/1,500\.00/)).toBeTruthy() }) it('una primaria, secundarias visibles y la destructiva bloqueada con el contador de REP', async () => { const { onAction } = setup() await waitFor(() => expect(screen.getAllByText('1').length).toBeGreaterThan(0)) // contador del botón inteligente fireEvent.click(screen.getByText('Registrar pago')) await waitFor(() => expect(onAction).toHaveBeenCalledTimes(1)) expect(onAction.mock.calls[0][0].def.key).toBe('pay') const cancel = screen.getByRole('button', { name: /Solicitar cancelación/ }) as HTMLButtonElement expect(cancel.disabled).toBe(true) fireEvent.click(cancel) expect(onAction).toHaveBeenCalledTimes(1) }) it('el botón inteligente abre la pestaña con las filas de la fuente', async () => { setup() await waitFor(() => expect(screen.getAllByText('1').length).toBeGreaterThan(0)) fireEvent.click(screen.getByRole('button', { name: /REP/ })) await waitFor(() => expect(screen.getByText('REP-UUID-1')).toBeTruthy()) }) it('openUrl abre el archivo sin pasar por onAction', async () => { const open = vi.spyOn(window, 'open').mockImplementation(() => null) const { onAction } = setup() fireEvent.click(screen.getByText('Descargar PDF')) await waitFor(() => expect(open).toHaveBeenCalledWith('https://x/y.pdf', '_blank', 'noopener,noreferrer')) expect(onAction).not.toHaveBeenCalled() }) it('el contador usa meta.total del servidor, no el tamaño de la página traída', async () => { const api = { get: vi.fn().mockResolvedValue({ data: { success: true, data: [{ id: 'r1', fiscal_uuid: 'REP-UUID-1' }], meta: { total: 312 } }, }), } as unknown as ApiClient render( , ) // 312 en el botón inteligente y en la pestaña; nunca «1» (una fila traída). await waitFor(() => expect(screen.getAllByText('312').length).toBeGreaterThan(0)) expect(screen.queryAllByText('1')).toHaveLength(0) }) })