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 AdminDashboardView from '../../resources/js/views/admin/AdminDashboardView.vue'; // Мокаем клиент дашборда: 3 GET-эндпоинта возвращают фикстуры. vi.mock('../../resources/js/api/adminDashboard', () => ({ getDashboardSummary: vi.fn().mockResolvedValue({ period: '7d', finance: { topups_rub: '320000', charges_rub: '180000', active_clients: 5, new_clients: 2, negative_balance_count: 1, light: 'red', }, health: { light: 'green', open_incidents: 0, job_errors_24h: 0, failed_jobs_24h: 0, last_sync_status: 'success', last_sync_at: null, }, leads: { light: 'green', delivered_today: 71, received_today: 80, stuck: 0, unrouted: 0 }, supply: { light: 'red', demand: 250, formula: 160, ordered: 175, mismatches: 1, total_orders: 405, total_limit: 5031, snapshot_date: '2026-06-28' }, balances: { light: 'red', count: 3, red: 1 }, clients: { light: 'amber', total_active: 6, new_count: 5, logged_in: 5, dormant: 1 }, }), getDashboardFinance: vi.fn().mockResolvedValue({ period: '7d', kpi: { topups_rub: '320000', charges_rub: '180000', net_inflow_rub: '140000', negative_balance_count: 1 }, attention: [{ id: 9, subdomain: 'romashka', organization_name: 'ООО Ромашка', balance_rub: '-4200', state: 'negative' }], top_by_turnover: [{ id: 2, organization_name: 'lkomega', topped_rub: '200000' }], }), getDashboardHealth: vi.fn().mockResolvedValue({ overall_light: 'green', subsystems: [ { key: 'queues', light: 'green', detail: '0 упавших за сутки' }, { key: 'incidents', light: 'green', detail: '0 открытых' }, ], }), getDashboardLeads: vi.fn().mockResolvedValue({ light: 'green', kpi: { delivered_today: 71, received_today: 80, stuck: 0, unrouted: 0 }, recent: [ { id: 501, received_at: '2026-06-28 07:55', platform: 'B1', channel: 'site', source: 'okna.ru', phone_masked: '79***07', delivered: true, processed: true }, ], }), getDashboardSupply: vi.fn().mockResolvedValue({ snapshot_date: '2026-06-28', light: 'red', totals: { demand: 250, formula: 160, ordered: 175, mismatches: 1 }, total_orders: 405, total_limit: 5031, groups: [{ signal_type: 'site', identifier: 'okna.ru', demand: 150, formula: 100, ordered: 100, in_sync: true }], }), getDashboardClients: vi.fn().mockResolvedValue({ kpi: { total_active: 6, new_count: 5, logged_in: 5, got_leads: 3, paid: 3 }, new_clients: [ { id: 11, organization_name: 'Новый Актив', subdomain: 'na', status: 'active', created_at: '2026-06-27', last_login_at: '2026-06-27 07:55', delivered_in_month: 3, balance_rub: '90' }, { id: 12, organization_name: 'Не Активировался', subdomain: 'naktiv', status: 'active', created_at: '2026-06-23', last_login_at: null, delivered_in_month: 0, balance_rub: '300' }, ], dormant: [ { id: 12, organization_name: 'Не Активировался', subdomain: 'naktiv', last_login_at: null, balance_rub: '300' }, ], }), getDashboardBalances: vi.fn().mockResolvedValue({ light: 'red', services: [ { service_key: 'yandex_cloud', balance_amount: '-540', currency: 'RUB', daily_spend_estimate: '600', days_left: 0, light: 'red', ok: true, error: null, checked_at: '2026-06-28T06:30:00Z', topup_url: 'https://console.yandex.cloud/billing/accounts/dn2w7fcvynjxe6elljct/payments' }, { service_key: 'dadata', balance_amount: '4500', currency: 'RUB', daily_spend_estimate: '100', days_left: 9, light: 'green', ok: true, error: null, checked_at: '2026-06-28T06:30:00Z', topup_url: 'https://dadata.ru/profile/#billing' }, { service_key: 'supplier', balance_amount: null, currency: 'RUB', daily_spend_estimate: null, days_left: null, light: 'grey', ok: false, error: 'кабинет недоступен', checked_at: '2026-06-28T06:30:00Z', topup_url: 'https://crm.bp-gr.ru' }, ], }), })); beforeEach(() => { vi.clearAllMocks(); }); describe('AdminDashboardView.vue', () => { const factory = async () => { const router = createRouter({ history: createMemoryHistory(), routes: [ { path: '/admin/dashboard', name: 'admin-dashboard', component: AdminDashboardView }, { path: '/admin/tenants/:code', name: 'admin-tenant-detail', component: { template: '
' } }, ], }); await router.push('/admin/dashboard'); await router.isReady(); const wrapper = mount(AdminDashboardView, { global: { plugins: [createVuetify(), router] }, }); await flushPromises(); await wrapper.vm.$nextTick(); return { wrapper, router }; }; it('монтируется и содержит заголовок «Командный центр»', async () => { const { wrapper } = await factory(); expect(wrapper.find('h1').text()).toBe('Командный центр'); }); it('рендерит 6 плиток: Финансы / Здоровье / Лиды / Заказ / Балансы / Клиенты', async () => { const { wrapper } = await factory(); const text = wrapper.text(); expect(text).toContain('Финансы'); expect(text).toContain('Здоровье портала'); expect(text).toContain('Лиды'); expect(text).toContain('Заказ у поставщика'); expect(text).toContain('Балансы сервисов'); expect(text).toContain('Клиенты'); }); it('плитка Клиенты показывает новых и спящих, drill — списки', async () => { const { wrapper } = await factory(); expect(wrapper.text()).toContain('1 спят'); // dormant=1 await wrapper.find('[data-testid="tile-clients"]').trigger('click'); await wrapper.vm.$nextTick(); expect(wrapper.find('[data-testid="drill-clients"]').exists()).toBe(true); expect(wrapper.text()).toContain('Не Активировался'); expect(wrapper.text()).toContain('ни разу'); // last_login_at=null }); it('плитка Балансы показывает сервисы и «на исходе»', async () => { const { wrapper } = await factory(); const text = wrapper.text(); expect(text).toContain('Yandex Cloud'); expect(text).toContain('DaData'); expect(text).toContain('1 на исходе'); // red=1 }); it('drill Балансов содержит кнопку «Пополнить» с прямой ссылкой на оплату', async () => { const { wrapper } = await factory(); await wrapper.find('[data-testid="tile-balances"]').trigger('click'); await wrapper.vm.$nextTick(); expect(wrapper.find('[data-testid="drill-balances"]').exists()).toBe(true); const topup = wrapper.find('[data-testid="topup-yandex_cloud"]'); expect(topup.exists()).toBe(true); expect(topup.attributes('href')).toBe('https://console.yandex.cloud/billing/accounts/dn2w7fcvynjxe6elljct/payments'); // упавший сервис показан как «не удалось обновить» expect(wrapper.text()).toContain('не удалось обновить'); }); it('плитки Лиды и Заказ показывают живые числа', async () => { const { wrapper } = await factory(); const text = wrapper.text(); expect(text).toContain('Доставлено сегодня'); expect(text).toContain('71'); expect(text).toContain('1 рассинхрон'); // светофор Заказа (mismatches=1) }); it('клик по плитке Заказ показывает таблицу групп', async () => { const { wrapper } = await factory(); await wrapper.find('[data-testid="tile-supply"]').trigger('click'); await wrapper.vm.$nextTick(); expect(wrapper.find('[data-testid="drill-supply"]').exists()).toBe(true); expect(wrapper.text()).toContain('okna.ru'); expect(wrapper.text()).toContain('По группам'); }); it('drill Лиды показывает последние лиды и ссылку «Открыть все лиды»', async () => { const { wrapper } = await factory(); await wrapper.find('[data-testid="tile-leads"]').trigger('click'); await wrapper.vm.$nextTick(); expect(wrapper.find('[data-testid="drill-leads"]').exists()).toBe(true); expect(wrapper.text()).toContain('okna.ru'); // recent lead source const link = wrapper.find('[data-testid="open-all-leads"]'); expect(link.exists()).toBe(true); expect(link.attributes('href')).toContain('/admin/leads'); }); it('Финансы и Здоровье показывают живые числа из API', async () => { const { wrapper } = await factory(); const text = wrapper.text(); expect(text).toMatch(/320\s+000\s*₽/); // пополнения expect(text).toContain('1 в минусе'); // светофор Финансов (red) expect(text).toContain('success'); // статус синхрона }); it('по умолчанию открыт drill Финансов с KPI «Чистый приток»', async () => { const { wrapper } = await factory(); expect(wrapper.find('[data-testid="drill-fin"]').exists()).toBe(true); expect(wrapper.text()).toMatch(/140\s+000\s*₽/); // net_inflow expect(wrapper.text()).toContain('ООО Ромашка'); // строка «внимание» }); it('клик по плитке Здоровье переключает drill на подсистемы', async () => { const { wrapper } = await factory(); await wrapper.find('[data-testid="tile-health"]').trigger('click'); await wrapper.vm.$nextTick(); expect(wrapper.find('[data-testid="drill-health"]').exists()).toBe(true); expect(wrapper.find('[data-testid="drill-fin"]').exists()).toBe(false); expect(wrapper.text()).toContain('Очереди / джобы'); }); it('клик по строке «внимание» уводит на карточку тенанта (Уровень 3)', async () => { const { wrapper, router } = await factory(); const push = vi.spyOn(router, 'push'); await wrapper.find('[data-testid="drill-fin"] tbody tr.clk').trigger('click'); await wrapper.vm.$nextTick(); expect(push).toHaveBeenCalledWith({ name: 'admin-tenant-detail', params: { code: 'romashka' } }); }); it('смена периода перезагружает данные (вызов summary дважды)', async () => { const { wrapper } = await factory(); const api = await import('../../resources/js/api/adminDashboard'); expect(api.getDashboardSummary).toHaveBeenCalledTimes(1); expect(api.getDashboardSummary).toHaveBeenLastCalledWith({ period: '7d' }); await wrapper.find('[data-testid="period-30d"]').trigger('click'); await flushPromises(); expect(api.getDashboardSummary).toHaveBeenCalledTimes(2); expect(api.getDashboardSummary).toHaveBeenLastCalledWith({ period: '30d' }); }); it('свой период шлёт date_from/date_to', async () => { const { wrapper } = await factory(); const api = await import('../../resources/js/api/adminDashboard'); await wrapper.find('[data-testid="period-custom-toggle"]').trigger('click'); await wrapper.vm.$nextTick(); wrapper.vm.dateFrom = '2026-06-01'; wrapper.vm.dateTo = '2026-06-15'; await wrapper.vm.$nextTick(); await wrapper.find('[data-testid="apply-custom"]').trigger('click'); await flushPromises(); expect(api.getDashboardSummary).toHaveBeenLastCalledWith({ date_from: '2026-06-01', date_to: '2026-06-15' }); }); it('API reject → fetch-error-alert виден', async () => { const api = await import('../../resources/js/api/adminDashboard'); vi.mocked(api.getDashboardSummary).mockRejectedValueOnce(new Error('Network')); const { wrapper } = await factory(); expect(wrapper.find('[data-testid="fetch-error-alert"]').exists()).toBe(true); }); });