Files
portal/app/tests/Frontend/SalesClientCard.spec.ts
T
Дмитрий dfb41751c8 feat(sales): показ дохода «Заработал» в списке/карточке/сводке (фронт)
Task 3.3b — три экрана Фазы 1 показывают живой earned_rub вместо
заглушки «—»:
- «Мои клиенты»: колонка «Заработал» = комиссия по клиенту (или «—»
  без привязки).
- Карточка клиента: KPI «Вы заработали (период)».
- Сводка: KPI «Я заработал» = суммарная комиссия за период.
Тип earned_rub расширен до number|null. Vitest-моки/ассерты обновлены
под реальные значения. Фронт-набор 1052 зелёных, ESLint чист.

Фаза 3 завершена: доход менеджера считается по тарифу каждого клиента
и виден во всех экранах.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 19:50:24 +03:00

227 lines
8.9 KiB
TypeScript

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<typeof vi.fn>).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: '<div/>' } },
],
});
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<typeof vi.fn>;
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('Юридическое лицо');
});
});