dfb41751c8
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>
153 lines
6.1 KiB
TypeScript
153 lines
6.1 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 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('Просрочка');
|
||
});
|
||
});
|