import { describe, it, expect, vi, beforeEach } from 'vitest'; import { mount, flushPromises } from '@vue/test-utils'; import { createVuetify } from 'vuetify'; import { createPinia } from 'pinia'; import { createRouter, createMemoryHistory } from 'vue-router'; import SalesOverviewView from '../../resources/js/views/sales/SalesOverviewView.vue'; // ─── mock getSalesOverview ──────────────────────────────────────────────────── // NOTE: vi.mock is hoisted, so the factory must not reference outer variables. vi.mock('../../resources/js/api/sales', () => ({ getSalesOverview: vi.fn().mockResolvedValue({ totals: { clients_count: 7, active: 5, trial: 1, overdue: 1, balance_sum_rub: '668870', leads_delivered: 1567, oborot_rub: 235050, earned_rub: 13350, }, attention: [ { tenant_id: 301, organization_name: 'Двери Премиум', status: 'overdue', balance_rub: '-1200', runway_days: 0, }, { tenant_id: 302, organization_name: 'Ремонт под ключ', status: 'suspended', balance_rub: '120', runway_days: null, }, ], top_clients: [ { tenant_id: 401, organization_name: 'Кухонная мебель СПб', leads_delivered: 412, oborot_rub: 61800 }, { tenant_id: 101, organization_name: 'Окна Москва ООО', leads_delivered: 331, oborot_rub: 49650 }, { tenant_id: 402, organization_name: 'Кухни на заказ Екб', leads_delivered: 286, oborot_rub: 42900 }, ], }), })); // Shared sample for use in individual tests const sampleOverview = { totals: { clients_count: 7, active: 5, trial: 1, overdue: 1, balance_sum_rub: '668870', leads_delivered: 1567, oborot_rub: 235050, earned_rub: null, }, attention: [ { tenant_id: 301, organization_name: 'Двери Премиум', status: 'overdue', balance_rub: '-1200', runway_days: 0, }, { tenant_id: 302, organization_name: 'Ремонт под ключ', status: 'suspended', balance_rub: '120', runway_days: null, }, ], top_clients: [ { tenant_id: 401, organization_name: 'Кухонная мебель СПб', leads_delivered: 412, oborot_rub: 61800 }, { tenant_id: 101, organization_name: 'Окна Москва ООО', leads_delivered: 331, oborot_rub: 49650 }, { tenant_id: 402, organization_name: 'Кухни на заказ Екб', leads_delivered: 286, oborot_rub: 42900 }, ], }; beforeEach(() => vi.clearAllMocks()); // ─── helpers ────────────────────────────────────────────────────────────────── function makeRouter() { const router = createRouter({ history: createMemoryHistory(), routes: [ { path: '/sales', name: 'sales-overview', component: SalesOverviewView }, { path: '/sales/clients', name: 'sales-clients', component: { template: '
' } }, { path: '/sales/clients/:id', name: 'sales-client-detail', component: { template: '' } }, { path: '/sales/attach', name: 'sales-attach', component: { template: '' } }, ], }); return router.push('/sales').then(() => router); } describe('SalesOverviewView', () => { it('отрисовывает KPI — количество клиентов', async () => { const router = await makeRouter(); const wrapper = mount(SalesOverviewView, { global: { plugins: [createVuetify(), createPinia(), router] }, }); await flushPromises(); const kpiClients = wrapper.find('[data-testid="kpi-clients"]'); expect(kpiClients.text()).toContain('7'); expect(kpiClients.text()).toContain('5 активных'); expect(kpiClients.text()).toContain('1 триал'); expect(kpiClients.text()).toContain('1 просрочка'); }); it('отрисовывает KPI — Σ баланс клиентов', async () => { const router = await makeRouter(); const wrapper = mount(SalesOverviewView, { global: { plugins: [createVuetify(), createPinia(), router] }, }); await flushPromises(); const kpiBalance = wrapper.find('[data-testid="kpi-balance"]'); // 668870 → «668 870 ₽» expect(kpiBalance.text()).toContain('₽'); expect(kpiBalance.text()).toContain('668'); }); it('отрисовывает KPI — лидов пришло', async () => { const router = await makeRouter(); const wrapper = mount(SalesOverviewView, { global: { plugins: [createVuetify(), createPinia(), router] }, }); await flushPromises(); const kpiLeads = wrapper.find('[data-testid="kpi-leads"]'); // toLocaleString(ru-RU) uses non-breaking space as thousands separator in ru-RU locale expect(kpiLeads.text()).toMatch(/1.567/); }); it('отрисовывает KPI — оборот', async () => { const router = await makeRouter(); const wrapper = mount(SalesOverviewView, { global: { plugins: [createVuetify(), createPinia(), router] }, }); await flushPromises(); const kpiOborot = wrapper.find('[data-testid="kpi-oborot"]'); expect(kpiOborot.text()).toContain('235'); expect(kpiOborot.text()).toContain('₽'); }); it('«Я заработал» показывает суммарную комиссию за период', async () => { const router = await makeRouter(); const wrapper = mount(SalesOverviewView, { global: { plugins: [createVuetify(), createPinia(), router] }, }); await flushPromises(); const earnedVal = wrapper.find('[data-testid="earned-value"]'); // earned_rub=13350 → «13 350 ₽» expect(earnedVal.text()).toContain('13'); expect(earnedVal.text()).toContain('₽'); }); it('таблица «Требуют внимания» отрисовывает строки', async () => { const router = await makeRouter(); const wrapper = mount(SalesOverviewView, { global: { plugins: [createVuetify(), createPinia(), router] }, }); await flushPromises(); const rows = wrapper.findAll('[data-testid="attention-row"]'); expect(rows.length).toBe(2); expect(rows[0].text()).toContain('Двери Премиум'); expect(rows[1].text()).toContain('Ремонт под ключ'); }); it('клик по строке «Требуют внимания» переходит в карточку клиента', async () => { const router = await makeRouter(); const pushSpy = vi.spyOn(router, 'push'); const wrapper = mount(SalesOverviewView, { global: { plugins: [createVuetify(), createPinia(), router] }, }); await flushPromises(); const rows = wrapper.findAll('[data-testid="attention-row"]'); expect(rows.length).toBeGreaterThan(0); await rows[0].trigger('click'); expect(pushSpy).toHaveBeenCalledWith('/sales/clients/301'); }); it('таблица «Топ клиентов» отрисовывает строки', async () => { const router = await makeRouter(); const wrapper = mount(SalesOverviewView, { global: { plugins: [createVuetify(), createPinia(), router] }, }); await flushPromises(); const rows = wrapper.findAll('[data-testid="top-client-row"]'); expect(rows.length).toBe(3); expect(rows[0].text()).toContain('Кухонная мебель СПб'); expect(rows[1].text()).toContain('Окна Москва ООО'); }); it('клик по строке «Топ клиентов» переходит в карточку клиента', async () => { const router = await makeRouter(); const pushSpy = vi.spyOn(router, 'push'); const wrapper = mount(SalesOverviewView, { global: { plugins: [createVuetify(), createPinia(), router] }, }); await flushPromises(); const rows = wrapper.findAll('[data-testid="top-client-row"]'); expect(rows.length).toBeGreaterThan(0); await rows[1].trigger('click'); expect(pushSpy).toHaveBeenCalledWith('/sales/clients/101'); }); it('вызывает getSalesOverview при монтировании', async () => { const router = await makeRouter(); mount(SalesOverviewView, { global: { plugins: [createVuetify(), createPinia(), router] }, }); await flushPromises(); const api = await import('../../resources/js/api/sales'); expect(api.getSalesOverview).toHaveBeenCalled(); }); it('статус «overdue» → чип «Просрочка»', async () => { const router = await makeRouter(); const wrapper = mount(SalesOverviewView, { global: { plugins: [createVuetify(), createPinia(), router] }, }); await flushPromises(); expect(wrapper.text()).toContain('Просрочка'); }); it('пустое состояние «attention» показывает «Все клиенты в порядке»', async () => { const { getSalesOverview } = await import('../../resources/js/api/sales'); vi.mocked(getSalesOverview).mockResolvedValueOnce({ ...sampleOverview, attention: [], }); const router = await makeRouter(); const wrapper = mount(SalesOverviewView, { global: { plugins: [createVuetify(), createPinia(), router] }, }); await flushPromises(); expect(wrapper.find('[data-testid="attention-empty"]').text()).toBe('Все клиенты в порядке'); }); });