d15a7cfd93
Фронт для правила «баланс блокирует запуск, а не создание»: - 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>
122 lines
4.7 KiB
TypeScript
122 lines
4.7 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||
import { mount } from '@vue/test-utils';
|
||
import { createPinia, setActivePinia } from 'pinia';
|
||
import { createVuetify } from 'vuetify';
|
||
import axios from 'axios';
|
||
|
||
import BulkActionsBar from '../../resources/js/components/projects/BulkActionsBar.vue';
|
||
import { useProjectsStore } from '../../resources/js/stores/projectsStore';
|
||
|
||
vi.mock('axios');
|
||
|
||
const defaultStubs = {
|
||
VDialog: { template: '<div><slot /></div>' },
|
||
DevIndexBadge: true,
|
||
RegionsBulkDialog: true,
|
||
DaysBulkDialog: true,
|
||
LimitBulkDialog: true,
|
||
};
|
||
|
||
beforeEach(() => {
|
||
setActivePinia(createPinia());
|
||
vi.clearAllMocks();
|
||
vi.mocked(axios.post).mockResolvedValue({ data: { updated: 0, skipped: [], warnings: [] } });
|
||
vi.mocked(axios.get).mockResolvedValue({ data: { data: [], meta: { total: 0 } } });
|
||
});
|
||
|
||
describe('BulkActionsBar — resume с balance_insufficient', () => {
|
||
it('resume с skipped balance_insufficient показывает тост «Запущено N, отложено M»', async () => {
|
||
setActivePinia(createPinia());
|
||
const store = useProjectsStore();
|
||
store.selectedIds.add(1);
|
||
store.selectedIds.add(2);
|
||
vi.spyOn(store, 'bulkUpdate').mockResolvedValue({
|
||
updated: 1,
|
||
skipped: [{ id: 2, reason: 'balance_insufficient' }],
|
||
warnings: [],
|
||
} as never);
|
||
window.confirm = vi.fn(() => true);
|
||
|
||
const wrapper = mount(BulkActionsBar, {
|
||
global: {
|
||
plugins: [createVuetify()],
|
||
stubs: defaultStubs,
|
||
},
|
||
});
|
||
|
||
await wrapper.find('[data-testid="bulk-resume"]').trigger('click');
|
||
await new Promise((r) => setTimeout(r, 30));
|
||
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||
const text = (wrapper.vm as any).skipToastText as string;
|
||
expect(text).toContain('1'); // updated
|
||
expect(text).toContain('1'); // skipped count
|
||
// Должен содержать «Запущено» и «отложено»
|
||
expect(text.toLowerCase()).toContain('запущено');
|
||
expect(text.toLowerCase()).toContain('отложено');
|
||
// Не должен говорить о лимите нового лимита (это update_limit причина)
|
||
expect(text.toLowerCase()).not.toContain('лимит');
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||
expect((wrapper.vm as any).skipToastOpen).toBe(true);
|
||
});
|
||
|
||
it('resume без skipped — тост не открывается', async () => {
|
||
setActivePinia(createPinia());
|
||
const store = useProjectsStore();
|
||
store.selectedIds.add(1);
|
||
vi.spyOn(store, 'bulkUpdate').mockResolvedValue({
|
||
updated: 1,
|
||
skipped: [],
|
||
warnings: [],
|
||
} as never);
|
||
window.confirm = vi.fn(() => true);
|
||
|
||
const wrapper = mount(BulkActionsBar, {
|
||
global: {
|
||
plugins: [createVuetify()],
|
||
stubs: defaultStubs,
|
||
},
|
||
});
|
||
|
||
await wrapper.find('[data-testid="bulk-resume"]').trigger('click');
|
||
await new Promise((r) => setTimeout(r, 30));
|
||
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||
expect((wrapper.vm as any).skipToastOpen).toBe(false);
|
||
});
|
||
|
||
it('resume со смешанными причинами — balance_insufficient даёт «отложено», другие — обычный текст', async () => {
|
||
setActivePinia(createPinia());
|
||
const store = useProjectsStore();
|
||
store.selectedIds.add(1);
|
||
store.selectedIds.add(2);
|
||
store.selectedIds.add(3);
|
||
vi.spyOn(store, 'bulkUpdate').mockResolvedValue({
|
||
updated: 1,
|
||
skipped: [
|
||
{ id: 2, reason: 'balance_insufficient' },
|
||
{ id: 3, reason: 'supplier_snapshot_locked' },
|
||
],
|
||
warnings: [],
|
||
} as never);
|
||
window.confirm = vi.fn(() => true);
|
||
|
||
const wrapper = mount(BulkActionsBar, {
|
||
global: {
|
||
plugins: [createVuetify()],
|
||
stubs: defaultStubs,
|
||
},
|
||
});
|
||
|
||
await wrapper.find('[data-testid="bulk-resume"]').trigger('click');
|
||
await new Promise((r) => setTimeout(r, 30));
|
||
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||
const text = (wrapper.vm as any).skipToastText as string;
|
||
// Оба skipped должны быть отражены
|
||
expect(text.toLowerCase()).toContain('отложено');
|
||
// supplier_snapshot_locked тоже должен быть
|
||
expect(text.toLowerCase()).toContain('сбор лидов');
|
||
});
|
||
});
|