Files
portal/app/tests/Frontend/AdminDashboardView.spec.ts
T
Дмитрий f30c6612c0 fix дашборд: достоверность метрик (здоровье/лиды/заказ) + периоды 60/90д
По сверке прод-данных с реальностью (часть чисел вводила в заблуждение):
- Финансы: +периоды 60 и 90 дней (крупные пополнения старше 30д теперь видны).
- Здоровье: «инциденты» больше не считают авто-лог ошибок джоб (summary
  'Автоматически:%') — раньше копилось 975 и держало красный ложно. Теперь:
  open_incidents = только реальные; добавлен job_errors_24h (повторяющиеся
  ошибки джоб за сутки) в подсистему queues.
- Лиды: убраны обманчивый «% доставки» (это было «обработано», не доставлено)
  и «нераспределённые по менеджерам» (менеджеры не используются). Добавлено
  «получено от поставщика сегодня»; доставлено = реально созданные сегодня сделки.
- Заказ: показаны дата снимка и полная картина (всего активных заказов /
  Σ лимита у поставщика) — сверка по снимку больше не выглядит занижено.

Тесты: admin-срез 87 зелёных, unit 3/3, фронт 10/10. stan 0, pint/eslint/
type-check/build чисто.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 18:57:35 +03:00

160 lines
7.6 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest';
import { mount, flushPromises } from '@vue/test-utils';
import { createVuetify } from 'vuetify';
import { createRouter, createMemoryHistory } from 'vue-router';
import AdminDashboardView from '../../resources/js/views/admin/AdminDashboardView.vue';
// Мокаем клиент дашборда: 3 GET-эндпоинта возвращают фикстуры.
vi.mock('../../resources/js/api/adminDashboard', () => ({
getDashboardSummary: vi.fn().mockResolvedValue({
period: '7d',
finance: {
topups_rub: '320000',
charges_rub: '180000',
active_clients: 5,
new_clients: 2,
negative_balance_count: 1,
light: 'red',
},
health: {
light: 'green',
open_incidents: 0,
job_errors_24h: 0,
failed_jobs_24h: 0,
last_sync_status: 'success',
last_sync_at: null,
},
leads: { light: 'green', delivered_today: 71, received_today: 80, stuck: 0, unrouted: 0 },
supply: { light: 'red', demand: 250, formula: 160, ordered: 175, mismatches: 1, total_orders: 405, total_limit: 5031, snapshot_date: '2026-06-28' },
}),
getDashboardFinance: vi.fn().mockResolvedValue({
period: '7d',
kpi: { topups_rub: '320000', charges_rub: '180000', net_inflow_rub: '140000', negative_balance_count: 1 },
attention: [{ id: 9, subdomain: 'romashka', organization_name: 'ООО Ромашка', balance_rub: '-4200', state: 'negative' }],
top_by_turnover: [{ id: 2, organization_name: 'lkomega', topped_rub: '200000' }],
}),
getDashboardHealth: vi.fn().mockResolvedValue({
overall_light: 'green',
subsystems: [
{ key: 'queues', light: 'green', detail: '0 упавших за сутки' },
{ key: 'incidents', light: 'green', detail: '0 открытых' },
],
}),
getDashboardLeads: vi.fn().mockResolvedValue({
light: 'green',
kpi: { delivered_today: 71, received_today: 80, stuck: 0, unrouted: 0 },
}),
getDashboardSupply: vi.fn().mockResolvedValue({
snapshot_date: '2026-06-28',
light: 'red',
totals: { demand: 250, formula: 160, ordered: 175, mismatches: 1 },
total_orders: 405,
total_limit: 5031,
groups: [{ signal_type: 'site', identifier: 'okna.ru', demand: 150, formula: 100, ordered: 100, in_sync: true }],
}),
}));
beforeEach(() => {
vi.clearAllMocks();
});
describe('AdminDashboardView.vue', () => {
const factory = async () => {
const router = createRouter({
history: createMemoryHistory(),
routes: [
{ path: '/admin/dashboard', name: 'admin-dashboard', component: AdminDashboardView },
{ path: '/admin/tenants/:code', name: 'admin-tenant-detail', component: { template: '<div />' } },
],
});
await router.push('/admin/dashboard');
await router.isReady();
const wrapper = mount(AdminDashboardView, {
global: { plugins: [createVuetify(), router] },
});
await flushPromises();
await wrapper.vm.$nextTick();
return { wrapper, router };
};
it('монтируется и содержит заголовок «Командный центр»', async () => {
const { wrapper } = await factory();
expect(wrapper.find('h1').text()).toBe('Командный центр');
});
it('рендерит 4 плитки: Финансы / Здоровье / Лиды / Заказ у поставщика', async () => {
const { wrapper } = await factory();
const text = wrapper.text();
expect(text).toContain('Финансы');
expect(text).toContain('Здоровье портала');
expect(text).toContain('Лиды');
expect(text).toContain('Заказ у поставщика');
});
it('плитки Лиды и Заказ показывают живые числа', async () => {
const { wrapper } = await factory();
const text = wrapper.text();
expect(text).toContain('Доставлено сегодня');
expect(text).toContain('71');
expect(text).toContain('1 рассинхрон'); // светофор Заказа (mismatches=1)
});
it('клик по плитке Заказ показывает таблицу групп', async () => {
const { wrapper } = await factory();
await wrapper.find('[data-testid="tile-supply"]').trigger('click');
await wrapper.vm.$nextTick();
expect(wrapper.find('[data-testid="drill-supply"]').exists()).toBe(true);
expect(wrapper.text()).toContain('okna.ru');
expect(wrapper.text()).toContain('По группам');
});
it('Финансы и Здоровье показывают живые числа из API', async () => {
const { wrapper } = await factory();
const text = wrapper.text();
expect(text).toMatch(/320\s+000\s*₽/); // пополнения
expect(text).toContain('1 в минусе'); // светофор Финансов (red)
expect(text).toContain('success'); // статус синхрона
});
it('по умолчанию открыт drill Финансов с KPI «Чистый приток»', async () => {
const { wrapper } = await factory();
expect(wrapper.find('[data-testid="drill-fin"]').exists()).toBe(true);
expect(wrapper.text()).toMatch(/140\s+000\s*₽/); // net_inflow
expect(wrapper.text()).toContain('ООО Ромашка'); // строка «внимание»
});
it('клик по плитке Здоровье переключает drill на подсистемы', async () => {
const { wrapper } = await factory();
await wrapper.find('[data-testid="tile-health"]').trigger('click');
await wrapper.vm.$nextTick();
expect(wrapper.find('[data-testid="drill-health"]').exists()).toBe(true);
expect(wrapper.find('[data-testid="drill-fin"]').exists()).toBe(false);
expect(wrapper.text()).toContain('Очереди / джобы');
});
it('клик по строке «внимание» уводит на карточку тенанта (Уровень 3)', async () => {
const { wrapper, router } = await factory();
const push = vi.spyOn(router, 'push');
await wrapper.find('[data-testid="drill-fin"] tbody tr.clk').trigger('click');
await wrapper.vm.$nextTick();
expect(push).toHaveBeenCalledWith({ name: 'admin-tenant-detail', params: { code: 'romashka' } });
});
it('смена периода перезагружает данные (вызов summary дважды)', async () => {
const { wrapper } = await factory();
const api = await import('../../resources/js/api/adminDashboard');
expect(api.getDashboardSummary).toHaveBeenCalledTimes(1);
await wrapper.find('[data-testid="period-30d"]').trigger('click');
await flushPromises();
expect(api.getDashboardSummary).toHaveBeenCalledTimes(2);
expect(api.getDashboardSummary).toHaveBeenLastCalledWith('30d');
});
it('API reject → fetch-error-alert виден', async () => {
const api = await import('../../resources/js/api/adminDashboard');
vi.mocked(api.getDashboardSummary).mockRejectedValueOnce(new Error('Network'));
const { wrapper } = await factory();
expect(wrapper.find('[data-testid="fetch-error-alert"]').exists()).toBe(true);
});
});