import { describe, it, expect, vi, beforeEach } from 'vitest'; import { mount, flushPromises } from '@vue/test-utils'; import { createVuetify } from 'vuetify'; import { createRouter, createMemoryHistory } from 'vue-router'; import AdminIncidentsView from '../../resources/js/views/admin/AdminIncidentsView.vue'; import { ADMIN_INCIDENTS } from '../../resources/js/composables/mockAdmin'; vi.mock('../../resources/js/api/admin', async (importOriginal) => { const orig = await importOriginal(); return { ...orig, listAdminIncidents: vi.fn(), }; }); const adminApi = await import('../../resources/js/api/admin'); beforeEach(() => { vi.clearAllMocks(); vi.mocked(adminApi.listAdminIncidents).mockResolvedValue({ incidents: ADMIN_INCIDENTS.map((r) => ({ id: r.id, incident_id: r.incident_id, type: r.category as string, severity: r.severity, summary: r.title, started_at: r.detected_at, detected_at: r.detected_at, resolved_at: null, status: (r.status === 'closed' ? 'resolved' : r.status) as 'open' | 'investigating' | 'resolved', affected_tenants_count: r.affected_tenants, affected_users_count: null, rkn_notified: r.rkn_notified, rkn_notified_at: null, rkn_deadline_at: r.rkn_deadline_at, })), total: ADMIN_INCIDENTS.length, limit: 100, offset: 0, summary: { open: ADMIN_INCIDENTS.filter((r) => r.status === 'open').length, investigating: ADMIN_INCIDENTS.filter((r) => r.status === 'investigating').length, rkn_pending: ADMIN_INCIDENTS.filter( (r) => ['pdn_breach', 'data_breach'].includes(r.category) && !r.rkn_notified, ).length, total_unresolved: ADMIN_INCIDENTS.filter((r) => r.status !== 'resolved' && r.status !== 'closed').length, }, }); }); const mountView = async () => { const router = createRouter({ history: createMemoryHistory(), routes: [ { path: '/admin/incidents', name: 'admin-incidents', component: AdminIncidentsView }, { path: '/admin/incidents/:id', name: 'admin-incident-detail', component: { template: '
' } }, ], }); await router.push('/admin/incidents'); await router.isReady(); const wrapper = mount(AdminIncidentsView, { global: { plugins: [createVuetify(), router] } }); await flushPromises(); return { wrapper, router }; }; describe('AdminIncidentsView.vue', () => { it('монтируется и содержит заголовок «Инциденты»', async () => { const { wrapper } = await mountView(); expect(wrapper.text()).toContain('Инциденты'); }); it('содержит 3 stats: Открыто / Расследуется / РКН-уведомлений', async () => { const { wrapper } = await mountView(); const text = wrapper.text(); expect(text).toContain('Открыто'); expect(text).toContain('Расследуется'); expect(text).toContain('РКН-уведомлений'); }); it('содержит фильтр-toggle по статусам (5 значений)', async () => { const { wrapper } = await mountView(); const text = wrapper.text(); expect(text).toContain('Все'); expect(text).toContain('Открыты'); expect(text).toContain('Решены'); expect(text).toContain('Закрыты'); }); it('показывает PDN-breach с РКН pending chip', async () => { const { wrapper } = await mountView(); const text = wrapper.text(); expect(text).toContain('Утечка ПДн'); expect(text).toContain('РКН pending'); }); it('содержит incident_id в формате INC-YYYY-MMDD-NNNN', async () => { const { wrapper } = await mountView(); const text = wrapper.text(); expect(text).toContain('INC-2026-0507-0034'); expect(text).toContain('INC-2026-0506-0028'); }); it('клик по строке инцидента вызывает router.push на admin-incident-detail', async () => { const { wrapper, router } = await mountView(); const pushSpy = vi.spyOn(router, 'push'); // get first row — populated via API mock const vm = wrapper.vm as unknown as { rowsState: Array<{ id: number }> }; const firstId = vm.rowsState[0].id; const row = wrapper.find(`[data-testid="incident-row-${firstId}"]`); expect(row.exists()).toBe(true); await row.trigger('click'); expect(pushSpy).toHaveBeenCalledWith({ name: 'admin-incident-detail', params: { id: firstId } }); }); });