Files
portal/app/tests/Frontend/ProjectsStoreToggle.spec.ts
T
Дмитрий 9ff7bc8fa0 feat(биллинг-фронт): баланс на запуске — сообщения в рублях, метка «не запущен», группы
Фронт для правила «баланс блокирует запуск, а не создание»:
- ProjectLimitOverloadDialog переформулирован в «Проект не запущен — пополните
  ~X ₽ или уменьшите объём» (сумма в рублях topup_rub).
- NewProjectDialog: создание всегда успешно; при launch.deferred показывает
  сообщение (не блок). Update-лимита 409 → тот же диалог.
- Возобновление (toggle-active) ловит 409 balance_insufficient (стор не
  переключает is_active), показывает диалог; постоянная метка «Не запущен —
  не хватает баланса» на карточке/в панели по preflight_blocked_at/balance_blocked.
- BulkActionsBar: сводка «Запущено N, отложено M — не хватает баланса» на resume.
- Автоподбор: CreateScreen показывает сводку запуска (рубли); FieldCompetitor
  массовые pause/resume через общий POST /api/projects/bulk (наследует BULK_MAX,
  слепок-защиту, балансовый гейт).
- ProjectResource отдаёт preflight_blocked_at.

Новые/обновлённые фронт-тесты зелёные. Пред-существующий красный baseline
фронт-набора (ErrorView/Settings/Legal/… + ProjectsView v-show) не трогался.

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

72 lines
2.8 KiB
TypeScript

import { describe, it, expect, beforeEach, vi } from 'vitest';
import { setActivePinia, createPinia } from 'pinia';
import axios from 'axios';
vi.mock('axios');
import { useProjectsStore } from '../../resources/js/stores/projectsStore';
const sampleProject = {
id: 42,
name: 'Test',
signal_type: 'site' as const,
signal_identifier: 'x.ru',
is_active: false,
daily_limit_target: 5,
delivered_today: 0,
sync_status: 'ok' as const,
preflight_blocked_at: null,
};
describe('projectsStore toggleActive', () => {
beforeEach(() => {
setActivePinia(createPinia());
vi.clearAllMocks();
});
it('при 409 balance_insufficient НЕ делает fetch и возвращает deferred+balance', async () => {
const balancePayload = {
current_balance_rub: '100.00',
current_capacity_leads: 1,
would_be_required_leads: 5,
deficit_leads: 4,
topup_rub: '400.00',
};
(axios.patch as unknown as ReturnType<typeof vi.fn>).mockRejectedValue({
response: { status: 409, data: { error: 'balance_insufficient', balance: balancePayload } },
});
(axios.get as unknown as ReturnType<typeof vi.fn>).mockResolvedValue({
data: { data: [], meta: { total: 0 } },
});
const store = useProjectsStore();
store.items = [{ ...sampleProject }] as any;
const result = await store.toggleActive({ ...sampleProject });
// Стор НЕ должен обновить is_active (fetch не вызывался при 409)
expect(result).toEqual({ deferred: true, balance: balancePayload });
expect(axios.get).not.toHaveBeenCalled(); // fetch НЕ вызван
// is_active в сторе не изменился
expect(store.items[0]?.is_active).toBe(false);
});
it('при успехе (200) делает fetch и возвращает void', async () => {
(axios.patch as unknown as ReturnType<typeof vi.fn>).mockResolvedValue({ data: { data: { id: 42 } } });
(axios.get as unknown as ReturnType<typeof vi.fn>).mockResolvedValue({
data: { data: [{ ...sampleProject, is_active: true }], meta: { total: 1 } },
});
const store = useProjectsStore();
const result = await store.toggleActive({ ...sampleProject });
expect(result).toBeUndefined();
expect(axios.get).toHaveBeenCalled(); // fetch вызван
});
it('другие ошибки (500) пробрасываются', async () => {
(axios.patch as unknown as ReturnType<typeof vi.fn>).mockRejectedValue({
response: { status: 500, data: { error: 'server_error' } },
});
const store = useProjectsStore();
await expect(store.toggleActive({ ...sampleProject })).rejects.toBeDefined();
});
});