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>
257 lines
10 KiB
TypeScript
257 lines
10 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 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: '<div/>' } },
|
|
{ path: '/sales/clients/:id', name: 'sales-client-detail', component: { template: '<div/>' } },
|
|
{ path: '/sales/attach', name: 'sales-attach', component: { template: '<div/>' } },
|
|
],
|
|
});
|
|
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('Все клиенты в порядке');
|
|
});
|
|
});
|