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 SalesClientCardView from '../../resources/js/views/sales/SalesClientCardView.vue'; import type { SalesClientCard } from '../../resources/js/api/sales'; // ─── sample data ────────────────────────────────────────────────────────────── const sampleCard: SalesClientCard = { profile: { organization_name: 'Окна Москва ООО', contact_email: 'info@okna-msk.ru', contact_name: 'Сергей Петров', contact_phone: '+7 905 123-45-67', inn: '7724444444', subject_type: 'legal_entity', created_at: '2026-04-12T10:00:00Z', desired_daily_numbers: 12, last_activity_at: '2026-06-28T09:41:00Z', }, kpi: { balance_rub: '14250', runway_days: 9, projects_count: 3, leads_delivered: 331, leads_target: 360, avg_lead_price_rub: 150.0, earned_rub: 2450, }, projects: [ { id: 1, name: 'Окна — Москва', signal_type: 'site', region: ['Москва'], daily_limit_target: 12, delivered_today: 11, status: 'active', }, { id: 2, name: 'Балконы', signal_type: 'sms', region: ['Москва', 'МО', 'Тверь', 'Калуга'], daily_limit_target: 4, delivered_today: 0, status: 'paused', }, ], leads_by_day: [ { date: '2026-06-28', count: 18, oborot_rub: 2700 }, { date: '2026-06-27', count: 22, oborot_rub: 3300 }, ], recent_leads: [ { received_at: '2026-06-28T09:41:00Z', phone_masked: '+7 905 •••-34-12', region: 'Москва', source: 'Сайт (B1)', project: 'Окна — Москва', }, { received_at: '2026-06-28T09:18:00Z', phone_masked: '+7 916 •••-77-08', region: 'Москва', source: 'Сайт (B1)', project: 'Окна — Москва', }, ], activity: [ { created_at: '2026-06-28T09:12:00Z', type: 'leads', amount_rub: '0', description: 'Доставлено 18 лидов', }, { created_at: '2026-06-27T19:40:00Z', type: 'topup', amount_rub: '10000', description: 'Пополнение баланса: + 10 000 ₽', }, ], }; // ─── mock getSalesClientCard ────────────────────────────────────────────────── vi.mock('../../resources/js/api/sales', () => ({ getSalesClientCard: vi.fn(), extractSalesErrorMessage: vi.fn((err: unknown, fb: string) => fb), })); beforeEach(async () => { vi.clearAllMocks(); const api = await import('../../resources/js/api/sales'); (api.getSalesClientCard as ReturnType).mockResolvedValue(sampleCard); }); // ─── helpers ────────────────────────────────────────────────────────────────── function makeRouter() { const router = createRouter({ history: createMemoryHistory(), routes: [ { path: '/sales/clients/:id', name: 'sales-client-detail', component: SalesClientCardView, props: true, }, { path: '/sales/clients', name: 'sales-clients', component: { template: '
' } }, ], }); return router.push('/sales/clients/101').then(() => router); } // ─── tests ──────────────────────────────────────────────────────────────────── describe('SalesClientCardView', () => { it('отрисовывает название организации', async () => { const router = await makeRouter(); const wrapper = mount(SalesClientCardView, { global: { plugins: [createVuetify(), createPinia(), router] }, }); await flushPromises(); expect(wrapper.text()).toContain('Окна Москва ООО'); expect(wrapper.find('[data-testid="org-name"]').text()).toBe('Окна Москва ООО'); }); it('KPI-плитка баланса отображает сумму', async () => { const router = await makeRouter(); const wrapper = mount(SalesClientCardView, { global: { plugins: [createVuetify(), createPinia(), router] }, }); await flushPromises(); const kpiBalance = wrapper.find('[data-testid="kpi-balance"]'); expect(kpiBalance.exists()).toBe(true); // 14250 → "14 250 ₽" (ru-RU formatting) expect(kpiBalance.text()).toContain('₽'); expect(kpiBalance.text()).toContain('250'); }); it('строка recent_lead показывает замаскированный телефон', async () => { const router = await makeRouter(); const wrapper = mount(SalesClientCardView, { global: { plugins: [createVuetify(), createPinia(), router] }, }); await flushPromises(); const phoneEls = wrapper.findAll('[data-testid="lead-phone"]'); expect(phoneEls.length).toBeGreaterThan(0); expect(phoneEls[0].text()).toBe('+7 905 •••-34-12'); expect(phoneEls[1].text()).toBe('+7 916 •••-77-08'); }); it('«Заработано» показывает доход по тарифу клиента', async () => { const router = await makeRouter(); const wrapper = mount(SalesClientCardView, { global: { plugins: [createVuetify(), createPinia(), router] }, }); await flushPromises(); const earned = wrapper.find('[data-testid="earned-value"]'); expect(earned.exists()).toBe(true); // earned_rub=2450 → «2 450,00 ₽» expect(earned.text()).toContain('2'); expect(earned.text()).toContain('₽'); }); it('список проектов отрисовывает строки', async () => { const router = await makeRouter(); const wrapper = mount(SalesClientCardView, { global: { plugins: [createVuetify(), createPinia(), router] }, }); await flushPromises(); const rows = wrapper.findAll('[data-testid="project-row"]'); expect(rows.length).toBe(2); expect(rows[0].text()).toContain('Окна — Москва'); expect(rows[1].text()).toContain('Балконы'); }); it('403 показывает сообщение «не закреплён»', async () => { const { getSalesClientCard } = await import('../../resources/js/api/sales'); const mockFn = getSalesClientCard as ReturnType; const axiosMock = await import('axios'); const axiosError = new axiosMock.default.AxiosError('Forbidden', '403'); Object.assign(axiosError, { response: { status: 403, data: {} } }); mockFn.mockRejectedValueOnce(axiosError); const router = await makeRouter(); const wrapper = mount(SalesClientCardView, { global: { plugins: [createVuetify(), createPinia(), router] }, }); await flushPromises(); const alert = wrapper.find('[data-testid="error-403"]'); expect(alert.exists()).toBe(true); expect(alert.text()).toContain('не закреплён'); }); it('вызывает getSalesClientCard при монтировании', async () => { const router = await makeRouter(); mount(SalesClientCardView, { global: { plugins: [createVuetify(), createPinia(), router] }, }); await flushPromises(); const api = await import('../../resources/js/api/sales'); expect(api.getSalesClientCard).toHaveBeenCalledWith('101', expect.any(Object)); }); it('тип лица «legal_entity» → «Юридическое лицо» в профиле', async () => { const router = await makeRouter(); const wrapper = mount(SalesClientCardView, { global: { plugins: [createVuetify(), createPinia(), router] }, }); await flushPromises(); expect(wrapper.text()).toContain('Юридическое лицо'); }); });