9ff7bc8fa0
Фронт для правила «баланс блокирует запуск, а не создание»: - 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>
72 lines
2.8 KiB
TypeScript
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();
|
|
});
|
|
});
|