Files
portal/app/tests/Frontend/BulkActionsBarResume.spec.ts
T
Дмитрий d15a7cfd93 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

122 lines
4.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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('сбор лидов');
});
});