Files
portal/app/tests/Frontend/SalesPerformance.spec.ts
T
Дмитрий e9edb206c8 @
feat(sales): кабинет начальника — единый период (без 500) + пополнения баланса

Экраны начальника отстали от вчерашних правок кабинета менеджера (§16–§21).

1. Живой баг: «Произвольный» период до выбора дат ронял запрос в 500 на пяти
   точках из шести (сводка отдела, доход, результативность, выплаты, тарифы).
   Разбор периода вынесен в трейт ResolvesSalesPeriod: 422 вместо падения,
   период по умолчанию d30 вместо this — как показывает сам PeriodPicker.

2. Пополнения баланса (topup_rub) добавлены в dashboard/overview (kpi +
   строки менеджеров) и managers/performance. На экранах: плашка «Пополнили
   баланс» в сводке отдела и колонка «Пополнили ₽» в обеих таблицах
   результативности. Из подписей убрано «(мес)» — период больше не месяц.

3. «Воронка отдела» правок не потребовала: она рендерит те же доску и карточку,
   что экран менеджера, а карточка сама грузит журнал и сохраняет контакты.

Тесты: SalesPeriodRequestTest проходит по всем шести точкам сразу.
Pest 263/263, Vitest 1420/1420, vue-tsc чист.
Спека — §22.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@
2026-07-18 15:20:44 +03:00

150 lines
5.7 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 SalesPerformanceView from '../../resources/js/views/sales/SalesPerformanceView.vue';
// ─── mock getSalesManagersPerformance ─────────────────────────────────────────
const perfMock = vi.fn().mockResolvedValue([
{
manager_id: 1,
name: 'Алексей Смирнов',
email: 'a.smirnov@liderra.ru',
clients_count: 7,
active_clients: 5,
leads_delivered: 1567,
oborot_rub: 235050,
topup_rub: 301000,
paid_all_time_rub: 85000,
earned_rub: 13350,
status: 'active',
},
{
manager_id: 3,
name: 'Игорь Лебедев',
email: 'i.lebedev@liderra.ru',
clients_count: 4,
active_clients: 3,
leads_delivered: 724,
oborot_rub: 108600,
topup_rub: 102800,
paid_all_time_rub: 40000,
earned_rub: 10000,
status: 'vacation',
},
]);
vi.mock('../../resources/js/api/sales', () => ({
getSalesManagersPerformance: (...args: unknown[]) => perfMock(...args),
}));
beforeEach(() => {
perfMock.mockClear();
});
// ─── helpers ──────────────────────────────────────────────────────────────────
function makeRouter() {
const router = createRouter({
history: createMemoryHistory(),
routes: [
{ path: '/sales/performance', name: 'sales-performance', component: SalesPerformanceView },
{ path: '/sales', name: 'sales-overview', component: { template: '<div/>' } },
],
});
return router.push('/sales/performance').then(() => router);
}
async function mountView() {
const router = await makeRouter();
const wrapper = mount(SalesPerformanceView, {
global: { plugins: [createVuetify(), createPinia(), router] },
});
await flushPromises();
return wrapper;
}
describe('SalesPerformanceView', () => {
it('отрисовывает заголовок и колонки', async () => {
const wrapper = await mountView();
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('Статус');
});
it('вызывает getSalesManagersPerformance при монтировании', async () => {
await mountView();
expect(perfMock).toHaveBeenCalled();
});
it('отрисовывает строки менеджеров из мока (имя + email)', async () => {
const wrapper = await mountView();
const rows = wrapper.findAll('[data-testid="perf-row"]');
expect(rows.length).toBe(2);
const text = wrapper.text();
expect(text).toContain('Алексей Смирнов');
expect(text).toContain('a.smirnov@liderra.ru');
expect(text).toContain('Игорь Лебедев');
expect(text).toContain('i.lebedev@liderra.ru');
});
it('статус active → чип «Активен», vacation → чип «Отпуск»', async () => {
const wrapper = await mountView();
const text = wrapper.text();
expect(text).toContain('Активен');
expect(text).toContain('Отпуск');
});
it('ввод в поиск вызывает getSalesManagersPerformance с параметром search (после debounce)', async () => {
vi.useFakeTimers();
try {
const router = await makeRouter();
const wrapper = mount(SalesPerformanceView, {
global: { plugins: [createVuetify(), createPinia(), router] },
});
// initial onMounted load
await vi.runAllTimersAsync();
await flushPromises();
perfMock.mockClear();
const input = wrapper.find('[data-testid="search-input"] input');
await input.setValue('Мария');
// Ждём завершения debounce.
await vi.runAllTimersAsync();
await flushPromises();
expect(perfMock).toHaveBeenCalled();
const lastCall = perfMock.mock.calls[perfMock.mock.calls.length - 1];
expect(lastCall[0]).toMatchObject({ search: 'Мария' });
} finally {
vi.useRealTimers();
}
});
});
describe('SalesPerformanceView — пополнения за период', () => {
it('показывает колонку пополнений по каждому менеджеру', async () => {
const router = await makeRouter();
const w = mount(SalesPerformanceView, {
global: { plugins: [createVuetify(), createPinia(), router] },
});
await flushPromises();
expect(w.find('[data-testid="perf-table"]').text()).toContain('Пополнили');
const first = w.findAll('[data-testid="perf-row"]')[0];
expect(first.text().replace(/\s/g, '')).toContain('301000');
});
});