93e8393014
- 5-я плитка дашборда со светофором (worst-of сервисов, поддержка grey=нет данных) - Drill-таблица: Сервис · Баланс · Хватит на N дней · Статус · кнопка «Пополнить» - Кнопка «Пополнить» (target=_blank) → страница оплаты сервиса; YC — прямо на биллинг - Клиент getDashboardBalances + типы; Vitest 12/12 (тайл, drill, href кнопки) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
190 lines
9.7 KiB
TypeScript
190 lines
9.7 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 },
|
|
}),
|
|
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 }],
|
|
}),
|
|
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('рендерит 5 плиток: Финансы / Здоровье / Лиды / Заказ / Балансы', async () => {
|
|
const { wrapper } = await factory();
|
|
const text = wrapper.text();
|
|
expect(text).toContain('Финансы');
|
|
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('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('Финансы и Здоровье показывают живые числа из 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);
|
|
});
|
|
});
|