import { describe, it, expect, vi, beforeEach } from 'vitest'; import { mount, flushPromises } from '@vue/test-utils'; import { createVuetify } from 'vuetify'; import { createPinia, setActivePinia } from 'pinia'; import DashboardView from '../../resources/js/views/DashboardView.vue'; import type { DashboardSummary } from '../../resources/js/api/dashboard'; import { useAuthStore } from '../../resources/js/stores/auth'; import type { AuthUser } from '../../resources/js/api/auth'; vi.mock('../../resources/js/api/dashboard', () => ({ getDashboardSummary: vi.fn(), })); const mockUser: AuthUser = { id: 1, email: 'user@liderra.ru', first_name: 'Иван', last_name: 'Петров', tenant_id: 1, totp_enabled: false, last_login_at: null, }; const dashboardApi = await import('../../resources/js/api/dashboard'); function makeSummary(overrides: Partial = {}): DashboardSummary { return { range: '7d', leads_received: { value: 247, delta_pct: 12.3, delta_dir: 'up' }, conversion: { value: 18.4, delta_pp: 2.1, delta_dir: 'up' }, active_projects: { active: 8 }, balance: { amount_rub: '14250.00', runway_days: 4, runway_leads: 285 }, activity: { points: [3, 5, 2, 8, 6, 9, 4], labels: ['сб', 'вс', 'пн', 'вт', 'ср', 'чт', 'сегодня'], max: 10 }, funnel: { new: 18, won: 45 }, avg_lead_cost_rub: null, ...overrides, }; } const mountView = () => { setActivePinia(createPinia()); useAuthStore().user = mockUser; return mount(DashboardView, { global: { plugins: [createVuetify()] } }); }; beforeEach(() => vi.clearAllMocks()); describe('DashboardView.vue ↔ /api/dashboard/summary', () => { it('getDashboardSummary вызывается на mount', async () => { vi.mocked(dashboardApi.getDashboardSummary).mockResolvedValueOnce(makeSummary()); mountView(); await flushPromises(); expect(dashboardApi.getDashboardSummary).toHaveBeenCalledTimes(1); }); it('успех — KPI и баланс из API видны', async () => { vi.mocked(dashboardApi.getDashboardSummary).mockResolvedValueOnce( makeSummary({ balance: { amount_rub: '99000.00', runway_days: 9, runway_leads: 500 } }), ); const wrapper = mountView(); await flushPromises(); const text = wrapper.text(); expect(text).toContain('Получено лидов'); expect(text).toContain('Конверсия в оплату'); expect(text).toContain('Активные проекты'); expect(text).toContain('Баланс'); expect(text).toContain('99 000'); expect(wrapper.text()).toContain('12.3%'); }); it('ошибка API — fallback на mock, view не падает', async () => { vi.mocked(dashboardApi.getDashboardSummary).mockRejectedValueOnce(new Error('500')); const wrapper = mountView(); await flushPromises(); expect(wrapper.text()).toContain('Получено лидов'); expect(wrapper.find('.runway-fill').exists()).toBe(true); expect(wrapper.find('[data-testid="dashboard-fetch-error"]').exists()).toBe(true); }); it('смена range перезапрашивает summary', async () => { vi.mocked(dashboardApi.getDashboardSummary).mockResolvedValue(makeSummary()); const wrapper = mountView(); await flushPromises(); expect(dashboardApi.getDashboardSummary).toHaveBeenCalledTimes(1); (wrapper.vm as unknown as { range: string }).range = '30d'; await flushPromises(); expect(dashboardApi.getDashboardSummary).toHaveBeenCalledTimes(2); }); // F3 (17.06.2026): runway_days в тексте — реальное число, не срезанное до // RUNWAY_MAX (7 сегментов полосы). Иначе дашборд расходится с биллингом. it('показывает реальный runway_days (не срезанный до 7 сегментов полосы)', async () => { vi.mocked(dashboardApi.getDashboardSummary).mockResolvedValueOnce( makeSummary({ balance: { amount_rub: '14250.00', runway_days: 28, runway_leads: 28 } }), ); const wrapper = mountView(); await flushPromises(); expect(wrapper.text()).toContain('28 дней'); }); }); describe('DashboardView — косяк 07: онбординг новичка', () => { beforeEach(() => localStorage.clear()); it('не показывает онбординг, когда есть активные проекты', async () => { vi.mocked(dashboardApi.getDashboardSummary).mockResolvedValueOnce(makeSummary()); const w = mountView(); await flushPromises(); expect(w.find('[data-testid="dashboard-onboarding"]').exists()).toBe(false); }); it('показывает онбординг новичку без проектов и лидов', async () => { vi.mocked(dashboardApi.getDashboardSummary).mockResolvedValueOnce( makeSummary({ active_projects: { active: 0 }, leads_received: { value: 0, delta_pct: 0, delta_dir: 'neutral' }, }), ); const w = mountView(); await flushPromises(); const card = w.find('[data-testid="dashboard-onboarding"]'); expect(card.exists()).toBe(true); expect(card.text()).toContain('Создать первый проект'); }); it('скрывает онбординг после «скрыть» и помнит это в localStorage', async () => { vi.mocked(dashboardApi.getDashboardSummary).mockResolvedValueOnce( makeSummary({ active_projects: { active: 0 }, leads_received: { value: 0, delta_pct: 0, delta_dir: 'neutral' }, }), ); const w = mountView(); await flushPromises(); expect(w.find('[data-testid="dashboard-onboarding"]').exists()).toBe(true); await w.find('[data-testid="onboarding-dismiss"]').trigger('click'); await w.vm.$nextTick(); expect(w.find('[data-testid="dashboard-onboarding"]').exists()).toBe(false); expect(localStorage.getItem('dashboard.onboardingDismissed')).toBe('1'); }); });