Files
portal/app/tests/Frontend/SalesClients.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

153 lines
6.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 SalesClientsView from '../../resources/js/views/sales/SalesClientsView.vue';
// ─── mock listSalesClients ────────────────────────────────────────────────────
vi.mock('../../resources/js/api/sales', () => ({
listSalesClients: vi.fn().mockResolvedValue([
{
tenant_id: 101,
organization_name: 'Окна Москва ООО',
inn: '7724444444',
subject_type: 'legal_entity',
last_activity_at: null,
balance_rub: '14250',
status: 'active',
tariff_name: 'Пополнения·Станд.',
projects_count: 3,
runway_days: 9,
leads_delivered: 331,
oborot_rub: 49650,
earned_rub: 4965,
},
{
tenant_id: 202,
organization_name: 'Натяжные потолки СПб',
inn: '7805123456',
subject_type: 'sole_proprietor',
last_activity_at: null,
balance_rub: '38100',
status: 'overdue',
tariff_name: 'Пополнения·Агро',
projects_count: 2,
runway_days: null,
leads_delivered: 228,
oborot_rub: 34200,
earned_rub: null,
},
]),
}));
beforeEach(() => vi.clearAllMocks());
// ─── helpers ──────────────────────────────────────────────────────────────────
function makeRouter() {
const router = createRouter({
history: createMemoryHistory(),
routes: [
{ path: '/sales/clients', name: 'sales-clients', component: SalesClientsView },
{ path: '/sales/clients/:id', name: 'sales-client-detail', component: { template: '<div/>' } },
{ path: '/sales', name: 'sales-overview', component: { template: '<div/>' } },
],
});
return router.push('/sales/clients').then(() => router);
}
describe('SalesClientsView', () => {
it('отрисовывает заголовки колонок', async () => {
const router = await makeRouter();
const wrapper = mount(SalesClientsView, {
global: { plugins: [createVuetify(), createPinia(), router] },
});
await flushPromises();
const text = wrapper.text();
expect(text).toContain('Клиент');
expect(text).toContain('Тип');
expect(text).toContain('Активность');
expect(text).toContain('Баланс');
expect(text).toContain('Запас');
expect(text).toContain('Проектов');
expect(text).toContain('Пришло');
expect(text).toContain('Оборот');
expect(text).toContain('Тариф');
expect(text).toContain('Заработал');
expect(text).toContain('Статус');
});
it('отрисовывает обе строки клиентов', async () => {
const router = await makeRouter();
const wrapper = mount(SalesClientsView, {
global: { plugins: [createVuetify(), createPinia(), router] },
});
await flushPromises();
const text = wrapper.text();
expect(text).toContain('Окна Москва ООО');
expect(text).toContain('7724444444');
expect(text).toContain('Натяжные потолки СПб');
expect(text).toContain('7805123456');
});
it('клик по строке вызывает router.push с /sales/clients/<tenant_id>', async () => {
const router = await makeRouter();
const pushSpy = vi.spyOn(router, 'push');
const wrapper = mount(SalesClientsView, {
global: { plugins: [createVuetify(), createPinia(), router] },
});
await flushPromises();
const rows = wrapper.findAll('[data-testid="client-row"]');
expect(rows.length).toBe(2);
await rows[0].trigger('click');
expect(pushSpy).toHaveBeenCalledWith('/sales/clients/101');
await rows[1].trigger('click');
expect(pushSpy).toHaveBeenCalledWith('/sales/clients/202');
});
it('колонка «Заработал» показывает доход или «—» без него', async () => {
const router = await makeRouter();
const wrapper = mount(SalesClientsView, {
global: { plugins: [createVuetify(), createPinia(), router] },
});
await flushPromises();
const earnedCells = wrapper.findAll('[data-testid="earned-cell"]');
expect(earnedCells.length).toBe(2);
// Первый клиент — с доходом (число), второй — без дохода → «—».
expect(earnedCells[0].text()).toContain('4');
expect(earnedCells[0].text()).toContain('₽');
expect(earnedCells[1].text().trim()).toBe('—');
});
it('вызывает listSalesClients при монтировании', async () => {
const router = await makeRouter();
mount(SalesClientsView, {
global: { plugins: [createVuetify(), createPinia(), router] },
});
await flushPromises();
const api = await import('../../resources/js/api/sales');
expect(api.listSalesClients).toHaveBeenCalled();
});
it('статус «active» → чип «Активен», «overdue» → чип «Просрочка»', async () => {
const router = await makeRouter();
const wrapper = mount(SalesClientsView, {
global: { plugins: [createVuetify(), createPinia(), router] },
});
await flushPromises();
const text = wrapper.text();
expect(text).toContain('Активен');
expect(text).toContain('Просрочка');
});
});