Files
portal/app/tests/Frontend/SalesBossOverview.spec.ts
T
Дмитрий 7cdbbdae46 feat(sales): сводка отдела (фронт)
Task 6.1b — SalesBossOverviewView (начальник, #page-boss-overview) поверх
GET /api/sales/dashboard/overview.

- 5 KPI (менеджеров с active/vacation, клиентов, Σ баланс, оборот отдела,
  выплачено за период).
- 4 кликабельные плитки-алерта → переходы: счета→/sales/invoices,
  заявки→/sales/requests, проблемы баланса→/sales/performance,
  провести выплату→/sales/payouts.
- Мини-таблица результативности менеджеров (клиенты/лиды/оборот/выплачено/
  заработал) с HelpHint. Период из salesPeriod store.
- Роут /sales/boss со заглушки на экран.

Vitest 12/12, фронт-набор 1078 без регрессий, ESLint чист.

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

233 lines
9.8 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 SalesBossOverviewView from '../../resources/js/views/sales/SalesBossOverviewView.vue';
// ─── mock getSalesDashboardOverview ────────────────────────────────────────────
// NOTE: vi.mock is hoisted, so the factory must not reference outer variables.
vi.mock('../../resources/js/api/sales', () => ({
getSalesDashboardOverview: vi.fn().mockResolvedValue({
kpi: {
managers_count: 4,
managers_active: 3,
managers_vacation: 1,
clients_count: 16,
balance_sum_rub: '1248600',
oborot_rub: 508950,
paid_period_rub: 125000,
},
alerts: {
invoices_awaiting: 2,
attachments_pending: 3,
clients_balance_problem: 5,
},
performance: [
{
manager_id: 11,
name: 'Алексей Смирнов',
clients_count: 7,
leads_delivered: 1567,
oborot_rub: 235050,
paid_all_time_rub: 85000,
earned_rub: 13350,
},
{
manager_id: 12,
name: 'Мария Котова',
clients_count: 5,
leads_delivered: 1102,
oborot_rub: 165300,
paid_all_time_rub: 60000,
earned_rub: 54795,
},
{
manager_id: 13,
name: 'Игорь Лебедев',
clients_count: 4,
leads_delivered: 724,
oborot_rub: 108600,
paid_all_time_rub: 40000,
earned_rub: 10000,
},
],
}),
}));
beforeEach(() => vi.clearAllMocks());
// ─── helpers ──────────────────────────────────────────────────────────────────
function makeRouter() {
const router = createRouter({
history: createMemoryHistory(),
routes: [
{ path: '/sales/boss', name: 'sales-boss', component: SalesBossOverviewView },
{ path: '/sales/invoices', name: 'sales-invoices', component: { template: '<div/>' } },
{ path: '/sales/requests', name: 'sales-requests', component: { template: '<div/>' } },
{ path: '/sales/performance', name: 'sales-performance', component: { template: '<div/>' } },
{ path: '/sales/payouts', name: 'sales-payouts', component: { template: '<div/>' } },
],
});
return router.push('/sales/boss').then(() => router);
}
describe('SalesBossOverviewView', () => {
it('вызывает getSalesDashboardOverview при монтировании', async () => {
const router = await makeRouter();
mount(SalesBossOverviewView, {
global: { plugins: [createVuetify(), createPinia(), router] },
});
await flushPromises();
const api = await import('../../resources/js/api/sales');
expect(api.getSalesDashboardOverview).toHaveBeenCalled();
});
it('отрисовывает KPI — Менеджеров с подписью активны/в отпуске', async () => {
const router = await makeRouter();
const wrapper = mount(SalesBossOverviewView, {
global: { plugins: [createVuetify(), createPinia(), router] },
});
await flushPromises();
const kpi = wrapper.find('[data-testid="kpi-managers"]');
expect(kpi.text()).toContain('4');
expect(kpi.text()).toContain('3 активны');
expect(kpi.text()).toContain('1 в отпуске');
});
it('отрисовывает KPI — Клиентов закреплено', async () => {
const router = await makeRouter();
const wrapper = mount(SalesBossOverviewView, {
global: { plugins: [createVuetify(), createPinia(), router] },
});
await flushPromises();
expect(wrapper.find('[data-testid="kpi-clients"]').text()).toContain('16');
});
it('отрисовывает KPI — Σ баланс клиентов', async () => {
const router = await makeRouter();
const wrapper = mount(SalesBossOverviewView, {
global: { plugins: [createVuetify(), createPinia(), router] },
});
await flushPromises();
const kpi = wrapper.find('[data-testid="kpi-balance"]');
expect(kpi.text()).toContain('₽');
expect(kpi.text()).toContain('248');
});
it('отрисовывает KPI — Оборот отдела', async () => {
const router = await makeRouter();
const wrapper = mount(SalesBossOverviewView, {
global: { plugins: [createVuetify(), createPinia(), router] },
});
await flushPromises();
const kpi = wrapper.find('[data-testid="kpi-oborot"]');
expect(kpi.text()).toContain('508');
expect(kpi.text()).toContain('₽');
});
it('отрисовывает KPI — Выплачено менеджерам', async () => {
const router = await makeRouter();
const wrapper = mount(SalesBossOverviewView, {
global: { plugins: [createVuetify(), createPinia(), router] },
});
await flushPromises();
const kpi = wrapper.find('[data-testid="kpi-paid"]');
expect(kpi.text()).toContain('125');
expect(kpi.text()).toContain('₽');
});
it('отрисовывает 4 плитки-алерта с числами', async () => {
const router = await makeRouter();
const wrapper = mount(SalesBossOverviewView, {
global: { plugins: [createVuetify(), createPinia(), router] },
});
await flushPromises();
const cards = wrapper.findAll('[data-alert-card]');
expect(cards.length).toBe(4);
expect(wrapper.find('[data-testid="alert-invoices"]').text()).toContain('2');
expect(wrapper.find('[data-testid="alert-invoices"]').text()).toContain('счёта ждут оплаты');
expect(wrapper.find('[data-testid="alert-requests"]').text()).toContain('3');
expect(wrapper.find('[data-testid="alert-requests"]').text()).toContain('заявки на привязку');
expect(wrapper.find('[data-testid="alert-balance"]').text()).toContain('5');
expect(wrapper.find('[data-testid="alert-balance"]').text()).toContain('баланса');
expect(wrapper.find('[data-testid="alert-payout"]').text()).toContain('провести выплату');
});
it('клик по плитке «счета ждут оплаты» ведёт на /sales/invoices', async () => {
const router = await makeRouter();
const pushSpy = vi.spyOn(router, 'push');
const wrapper = mount(SalesBossOverviewView, {
global: { plugins: [createVuetify(), createPinia(), router] },
});
await flushPromises();
await wrapper.find('[data-testid="alert-invoices"]').trigger('click');
expect(pushSpy).toHaveBeenCalledWith('/sales/invoices');
});
it('клик по плитке «заявки на привязку» ведёт на /sales/requests', async () => {
const router = await makeRouter();
const pushSpy = vi.spyOn(router, 'push');
const wrapper = mount(SalesBossOverviewView, {
global: { plugins: [createVuetify(), createPinia(), router] },
});
await flushPromises();
await wrapper.find('[data-testid="alert-requests"]').trigger('click');
expect(pushSpy).toHaveBeenCalledWith('/sales/requests');
});
it('клик по плитке «проблема баланса» ведёт на /sales/performance', async () => {
const router = await makeRouter();
const pushSpy = vi.spyOn(router, 'push');
const wrapper = mount(SalesBossOverviewView, {
global: { plugins: [createVuetify(), createPinia(), router] },
});
await flushPromises();
await wrapper.find('[data-testid="alert-balance"]').trigger('click');
expect(pushSpy).toHaveBeenCalledWith('/sales/performance');
});
it('клик по плитке «провести выплату» ведёт на /sales/payouts', async () => {
const router = await makeRouter();
const pushSpy = vi.spyOn(router, 'push');
const wrapper = mount(SalesBossOverviewView, {
global: { plugins: [createVuetify(), createPinia(), router] },
});
await flushPromises();
await wrapper.find('[data-testid="alert-payout"]').trigger('click');
expect(pushSpy).toHaveBeenCalledWith('/sales/payouts');
});
it('таблица результативности менеджеров отрисовывает строки', async () => {
const router = await makeRouter();
const wrapper = mount(SalesBossOverviewView, {
global: { plugins: [createVuetify(), createPinia(), router] },
});
await flushPromises();
const rows = wrapper.findAll('[data-testid="performance-row"]');
expect(rows.length).toBe(3);
expect(rows[0].text()).toContain('Алексей Смирнов');
expect(rows[1].text()).toContain('Мария Котова');
expect(rows[2].text()).toContain('Игорь Лебедев');
// числовые ячейки первой строки
expect(rows[0].text()).toContain('7');
expect(rows[0].text()).toContain('235');
expect(rows[0].text()).toContain('13');
});
});