/** * Tests de la sección "Automatización" del ActivityPanel. * * Los mocks del store usan vi.hoisted() para crear referencias estables: * si el mock devuelve una nueva referencia de activities en cada render, * el useEffect de ActivityPanel entra en loop infinito. */ import { describe, it, expect, vi, beforeEach } from 'vitest' import { render, screen, fireEvent, act } from '@testing-library/react' import { TooltipProvider } from '@/components/ui/tooltip' // ─── Mocks con referencias estables vía vi.hoisted ─────────────────────────── const processStoreMock = vi.hoisted(() => { const activity = { id: 'act-1', processId: 'p1', bpmnElementId: 'task1', name: 'Facturación', type: 'task' as const, directCostFixed: 200, executionTimeMinutes: 60, assignedResources: [], automatable: false, automatedCostFixed: 0, automatedTimeMinutes: 0, } return { activities: [activity], // referencia estable — no cambia entre renders resources: [], updateActivity: vi.fn(), subscribe: vi.fn(), // requerido por simulation-store.ts al importarse } }) const simulationStoreMock = vi.hoisted(() => ({ resultActual: null as null | object, resultAutomated: null as null | object, invalidateResults: vi.fn(), })) vi.mock('@/store/process-store', () => ({ useProcessStore: () => processStoreMock, // Zustand también expone .subscribe() como método estático del hook useProcessStore_subscribe: vi.fn(), })) vi.mock('@/store/simulation-store', () => ({ useSimulationStore: () => simulationStoreMock, })) import { ActivityPanel } from '@/features/workspace/ActivityPanel' // ─── Helper ─────────────────────────────────────────────────────────────────── function renderPanel(selectedElementId = 'task1') { return render( ) } // ─── Tests ──────────────────────────────────────────────────────────────────── beforeEach(() => { // Resetear el mock de updateActivity y restaurar la actividad base processStoreMock.updateActivity.mockReset() processStoreMock.updateActivity.mockResolvedValue(undefined) processStoreMock.activities[0].automatable = false processStoreMock.activities[0].automatedCostFixed = 0 processStoreMock.activities[0].automatedTimeMinutes = 0 simulationStoreMock.resultActual = null simulationStoreMock.resultAutomated = null simulationStoreMock.invalidateResults.mockReset() }) describe('ActivityPanel — sección Automatización', () => { it('sección Automatización está presente y toggle en OFF por defecto', () => { renderPanel() expect(screen.getByText('¿Es automatizable?')).toBeInTheDocument() const toggle = screen.getByRole('switch', { name: /automatizable/i }) expect(toggle).toHaveAttribute('aria-checked', 'false') }) it('Toggle ON: aria-checked cambia a true', async () => { renderPanel() const toggle = screen.getByRole('switch', { name: /automatizable/i }) await act(async () => { fireEvent.click(toggle) }) expect(toggle).toHaveAttribute('aria-checked', 'true') }) it('Toggle ON con ambos campos en 0: helper ámbar está en el DOM y visible (opacity-100)', async () => { renderPanel() const toggle = screen.getByRole('switch', { name: /automatizable/i }) await act(async () => { fireEvent.click(toggle) }) // El helper siempre está en el DOM — verificamos que el texto existe y el contenedor es visible const helperText = screen.getByText(/Transparencia metodológica/i) expect(helperText).toBeInTheDocument() // El contenedor padre tiene opacity-100 cuando showHelper=true const helperContainer = helperText.closest('[aria-hidden]') expect(helperContainer).toHaveAttribute('aria-hidden', 'false') }) it('Toggle ON con costo > 0: helper ámbar está en el DOM pero oculto (aria-hidden=true)', async () => { renderPanel() const toggle = screen.getByRole('switch', { name: /automatizable/i }) await act(async () => { fireEvent.click(toggle) }) const costoInput = screen.getByLabelText(/Costo automatizado/i) await act(async () => { fireEvent.change(costoInput, { target: { value: '50' } }) }) // El texto sigue en el DOM (transición CSS, no unmount) const helperText = screen.getByText(/Transparencia metodológica/i) expect(helperText).toBeInTheDocument() // Pero el contenedor está marcado aria-hidden=true (oculto visualmente) const helperContainer = helperText.closest('[aria-hidden]') expect(helperContainer).toHaveAttribute('aria-hidden', 'true') }) it('Editar costo automatizado y guardar llama updateActivity con el valor correcto', async () => { renderPanel() const toggle = screen.getByRole('switch', { name: /automatizable/i }) await act(async () => { fireEvent.click(toggle) }) const costoInput = screen.getByLabelText(/Costo automatizado/i) await act(async () => { fireEvent.change(costoInput, { target: { value: '25' } }) }) const guardarBtn = screen.getByRole('button', { name: /Guardar/i }) await act(async () => { fireEvent.click(guardarBtn) }) expect(processStoreMock.updateActivity).toHaveBeenCalledOnce() const arg = processStoreMock.updateActivity.mock.calls[0][0] expect(arg.automatedCostFixed).toBe(25) expect(arg.automatable).toBe(true) }) it('Guardar llama updateActivity (la invalidación reactiva es responsabilidad del store)', async () => { renderPanel() const toggle = screen.getByRole('switch', { name: /automatizable/i }) await act(async () => { fireEvent.click(toggle) }) const tiempoInput = screen.getByLabelText(/Tiempo automatizado/i) await act(async () => { fireEvent.change(tiempoInput, { target: { value: '5' } }) }) const guardarBtn = screen.getByRole('button', { name: /Guardar/i }) await act(async () => { fireEvent.click(guardarBtn) }) expect(processStoreMock.updateActivity).toHaveBeenCalledOnce() const arg = processStoreMock.updateActivity.mock.calls[0][0] expect(arg.automatedTimeMinutes).toBe(5) }) })