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>
107 lines
3.9 KiB
TypeScript
107 lines
3.9 KiB
TypeScript
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||
import { mount } from '@vue/test-utils';
|
||
import { setActivePinia, createPinia } from 'pinia';
|
||
|
||
vi.mock('../../resources/js/api/autopodbor');
|
||
import * as api from '../../resources/js/api/autopodbor';
|
||
import { useAutopodborStore } from '../../resources/js/stores/autopodborStore';
|
||
import CreateScreen from '../../resources/js/views/autopodbor/screens/CreateScreen.vue';
|
||
|
||
// Минимальный nav inject
|
||
const makeNav = (overrides: Record<string, unknown> = {}) => ({
|
||
go: vi.fn(),
|
||
ctx: {
|
||
selectedSourceIds: [1, 2],
|
||
loadMsg: '',
|
||
loadSub: '',
|
||
createdCount: 0,
|
||
launched: false,
|
||
...overrides,
|
||
},
|
||
screen: { value: 'create' },
|
||
});
|
||
|
||
const stubs = {
|
||
DevIndexBadge: true,
|
||
};
|
||
|
||
beforeEach(() => {
|
||
setActivePinia(createPinia());
|
||
vi.clearAllMocks();
|
||
});
|
||
|
||
describe('CreateScreen — launch summary (Task 17a)', () => {
|
||
it('deferred > 0 после создания — показывается сводка с суммой пополнения', async () => {
|
||
const nav = makeNav();
|
||
|
||
// Мокаем createProjects с launch.deferred > 0
|
||
(api.createProjects as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||
data: [
|
||
{ id: 1, name: 'Конкурент A', is_active: true },
|
||
{ id: 2, name: 'Конкурент B', is_active: false },
|
||
],
|
||
launch: {
|
||
launched: 1,
|
||
deferred: 1,
|
||
balance: { topup_rub: 350 },
|
||
},
|
||
});
|
||
|
||
// Нужен currentRun для regionCode
|
||
const store = useAutopodborStore();
|
||
store.currentRun = { id: 5, kind: 'search', status: 'done', region_code: 16 } as never;
|
||
store.sources = [
|
||
{ id: 1, competitor_id: 10, signal_type: 'site', identifier: 'a.ru', box: 'field', project: null } as never,
|
||
{ id: 2, competitor_id: 10, signal_type: 'site', identifier: 'b.ru', box: 'field', project: null } as never,
|
||
];
|
||
|
||
const wrapper = mount(CreateScreen, {
|
||
global: {
|
||
provide: { autopodborNav: nav },
|
||
stubs,
|
||
},
|
||
});
|
||
|
||
// Установить регион и нажать «Создать и запустить»
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||
(wrapper.vm as any).regionCode = 16;
|
||
await wrapper.find('.ld-btn-primary').trigger('click');
|
||
await new Promise((r) => setTimeout(r, 30));
|
||
|
||
// Должна показаться сводка с «отложено» и суммой
|
||
const text = wrapper.text();
|
||
expect(text.toLowerCase()).toMatch(/отложено|deferred|пополните/i);
|
||
expect(text).toMatch(/350/);
|
||
});
|
||
|
||
it('deferred = 0 — навигация на done, никакой сводки', async () => {
|
||
const nav = makeNav();
|
||
|
||
(api.createProjects as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||
data: [{ id: 1, name: 'Конкурент A', is_active: true }],
|
||
launch: { launched: 1, deferred: 0, balance: null },
|
||
});
|
||
|
||
const store = useAutopodborStore();
|
||
store.currentRun = { id: 5, kind: 'search', status: 'done', region_code: 16 } as never;
|
||
store.sources = [
|
||
{ id: 1, competitor_id: 10, signal_type: 'site', identifier: 'a.ru', box: 'field', project: null } as never,
|
||
];
|
||
|
||
const wrapper = mount(CreateScreen, {
|
||
global: {
|
||
provide: { autopodborNav: nav },
|
||
stubs,
|
||
},
|
||
});
|
||
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||
(wrapper.vm as any).regionCode = 16;
|
||
await wrapper.find('.ld-btn-primary').trigger('click');
|
||
await new Promise((r) => setTimeout(r, 30));
|
||
|
||
// nav.go('done') должен быть вызван
|
||
expect(nav.go).toHaveBeenCalledWith('done');
|
||
});
|
||
});
|