Files
portal/app/tests/Frontend/AdminDashboardView.spec.ts
T
Дмитрий 6536c19c96 feat(дашборд): Этап A — сквозная вложенность Лиды до источника
Экран «Лиды» (/admin/leads): серверный список с фильтрами (дата/канал/поставщик/
статус/поиск) + пагинация (масштаб 10⁴+ лидов). Карточка лида (/admin/leads/{id}):
полная цепочка — ОТКУДА (поставщик B1/B2/B3 + канал + источник + регион) → КОМУ
(сделки клиентов через deals.source_crm_id = supplier_leads.vid). Дашборд: drill
Лиды +топ-10 последних + «Открыть все лиды →». Nav-пункт «Лиды». ПДн-телефон
маскируется (152-ФЗ). Тесты: backend 3 + FE 5 (38 FE всего зелёные).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 10:14:47 +03:00

240 lines
13 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' },
balances: { light: 'red', count: 3, red: 1 },
clients: { light: 'amber', total_active: 6, new_count: 5, logged_in: 5, dormant: 1 },
}),
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 },
recent: [
{ id: 501, received_at: '2026-06-28 07:55', platform: 'B1', channel: 'site', source: 'okna.ru', phone_masked: '79***07', delivered: true, processed: true },
],
}),
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 }],
}),
getDashboardClients: vi.fn().mockResolvedValue({
kpi: { total_active: 6, new_count: 5, logged_in: 5, got_leads: 3, paid: 3 },
new_clients: [
{ id: 11, organization_name: 'Новый Актив', subdomain: 'na', status: 'active', created_at: '2026-06-27', last_login_at: '2026-06-27 07:55', delivered_in_month: 3, balance_rub: '90' },
{ id: 12, organization_name: 'Не Активировался', subdomain: 'naktiv', status: 'active', created_at: '2026-06-23', last_login_at: null, delivered_in_month: 0, balance_rub: '300' },
],
dormant: [
{ id: 12, organization_name: 'Не Активировался', subdomain: 'naktiv', last_login_at: null, balance_rub: '300' },
],
}),
getDashboardBalances: vi.fn().mockResolvedValue({
light: 'red',
services: [
{ service_key: 'yandex_cloud', balance_amount: '-540', currency: 'RUB', daily_spend_estimate: '600', days_left: 0, light: 'red', ok: true, error: null, checked_at: '2026-06-28T06:30:00Z', topup_url: 'https://console.yandex.cloud/billing/accounts/dn2w7fcvynjxe6elljct/payments' },
{ service_key: 'dadata', balance_amount: '4500', currency: 'RUB', daily_spend_estimate: '100', days_left: 9, light: 'green', ok: true, error: null, checked_at: '2026-06-28T06:30:00Z', topup_url: 'https://dadata.ru/profile/#billing' },
{ service_key: 'supplier', balance_amount: null, currency: 'RUB', daily_spend_estimate: null, days_left: null, light: 'grey', ok: false, error: 'кабинет недоступен', checked_at: '2026-06-28T06:30:00Z', topup_url: 'https://crm.bp-gr.ru' },
],
}),
}));
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('рендерит 6 плиток: Финансы / Здоровье / Лиды / Заказ / Балансы / Клиенты', async () => {
const { wrapper } = await factory();
const text = wrapper.text();
expect(text).toContain('Финансы');
expect(text).toContain('Здоровье портала');
expect(text).toContain('Лиды');
expect(text).toContain('Заказ у поставщика');
expect(text).toContain('Балансы сервисов');
expect(text).toContain('Клиенты');
});
it('плитка Клиенты показывает новых и спящих, drill — списки', async () => {
const { wrapper } = await factory();
expect(wrapper.text()).toContain('1 спят'); // dormant=1
await wrapper.find('[data-testid="tile-clients"]').trigger('click');
await wrapper.vm.$nextTick();
expect(wrapper.find('[data-testid="drill-clients"]').exists()).toBe(true);
expect(wrapper.text()).toContain('Не Активировался');
expect(wrapper.text()).toContain('ни разу'); // last_login_at=null
});
it('плитка Балансы показывает сервисы и «на исходе»', async () => {
const { wrapper } = await factory();
const text = wrapper.text();
expect(text).toContain('Yandex Cloud');
expect(text).toContain('DaData');
expect(text).toContain('1 на исходе'); // red=1
});
it('drill Балансов содержит кнопку «Пополнить» с прямой ссылкой на оплату', async () => {
const { wrapper } = await factory();
await wrapper.find('[data-testid="tile-balances"]').trigger('click');
await wrapper.vm.$nextTick();
expect(wrapper.find('[data-testid="drill-balances"]').exists()).toBe(true);
const topup = wrapper.find('[data-testid="topup-yandex_cloud"]');
expect(topup.exists()).toBe(true);
expect(topup.attributes('href')).toBe('https://console.yandex.cloud/billing/accounts/dn2w7fcvynjxe6elljct/payments');
// упавший сервис показан как «не удалось обновить»
expect(wrapper.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('drill Лиды показывает последние лиды и ссылку «Открыть все лиды»', async () => {
const { wrapper } = await factory();
await wrapper.find('[data-testid="tile-leads"]').trigger('click');
await wrapper.vm.$nextTick();
expect(wrapper.find('[data-testid="drill-leads"]').exists()).toBe(true);
expect(wrapper.text()).toContain('okna.ru'); // recent lead source
const link = wrapper.find('[data-testid="open-all-leads"]');
expect(link.exists()).toBe(true);
expect(link.attributes('href')).toContain('/admin/leads');
});
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);
expect(api.getDashboardSummary).toHaveBeenLastCalledWith({ period: '7d' });
await wrapper.find('[data-testid="period-30d"]').trigger('click');
await flushPromises();
expect(api.getDashboardSummary).toHaveBeenCalledTimes(2);
expect(api.getDashboardSummary).toHaveBeenLastCalledWith({ period: '30d' });
});
it('свой период шлёт date_from/date_to', async () => {
const { wrapper } = await factory();
const api = await import('../../resources/js/api/adminDashboard');
await wrapper.find('[data-testid="period-custom-toggle"]').trigger('click');
await wrapper.vm.$nextTick();
wrapper.vm.dateFrom = '2026-06-01';
wrapper.vm.dateTo = '2026-06-15';
await wrapper.vm.$nextTick();
await wrapper.find('[data-testid="apply-custom"]').trigger('click');
await flushPromises();
expect(api.getDashboardSummary).toHaveBeenLastCalledWith({ date_from: '2026-06-01', date_to: '2026-06-15' });
});
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);
});
});