/**
* Task 13: при ответе создания с launch.deferred=1
* показывается диалог перегрузки (проект считается созданным).
*
* Контракт POST /api/projects → 201:
* { data: {...}, launch: { launched: 0|1, deferred: 0|1, balance: OverloadPayload | null } }
*
* Если launch.deferred === 1:
* - форма закрывается (emit saved)
* - открывается ProjectLimitOverloadDialog с payload = launch.balance
*
* Если launch.deferred === 0: обычный успех, диалога нет.
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { mount, flushPromises } from '@vue/test-utils';
import { createPinia, setActivePinia } from 'pinia';
import { createVuetify } from 'vuetify';
vi.mock('../../resources/js/api/client', () => ({
apiClient: {
post: vi.fn(),
patch: vi.fn().mockResolvedValue({ data: {} }),
},
ensureCsrfCookie: vi.fn().mockResolvedValue(undefined),
extractErrorMessage: vi.fn(() => 'Произошла ошибка.'),
}));
vi.mock('../../resources/js/api/requisites', () => ({
getRequisites: vi.fn().mockResolvedValue({
subject_type: 'individual',
contact_name: 'Иван',
contact_phone: '+79161234567',
inn: null,
}),
updateRequisites: vi.fn().mockResolvedValue({}),
}));
// vue-router: stub для ProjectLimitOverloadDialog.goTopup
vi.mock('vue-router', () => ({
useRouter: vi.fn(() => ({ push: vi.fn() })),
}));
import { apiClient } from '../../resources/js/api/client';
import NewProjectDialog from '../../resources/js/views/projects/NewProjectDialog.vue';
import type { Project } from '../../resources/js/stores/projectsStore';
const factory = (
props: { modelValue: boolean; mode?: 'create' | 'edit'; project?: Project | null } = {
modelValue: true,
mode: 'create',
},
) =>
mount(NewProjectDialog, {
props,
global: {
plugins: [createVuetify()],
stubs: {
VDialog: {
template: '
',
props: ['modelValue'],
},
},
},
});
beforeEach(() => {
setActivePinia(createPinia());
vi.clearAllMocks();
});
describe('NewProjectDialog — launch.deferred (Task 13)', () => {
it('при launch.deferred=1 открывается диалог перегрузки и эмитится saved', async () => {
// Бэкенд вернул 201 с deferred=1
(apiClient.post as ReturnType).mockResolvedValueOnce({
data: {
data: { id: 42, name: 'Тест' },
launch: {
launched: 0,
deferred: 1,
balance: {
current_balance_rub: '500.00',
current_capacity_leads: 15,
would_be_required_leads: 50,
deficit_leads: 35,
topup_rub: '1050.00',
},
},
},
});
const wrapper = factory();
await flushPromises();
const vm = wrapper.vm as unknown as {
confirmVsyaRf: () => void;
form: { name: string; signal_identifier: string };
submit: () => Promise;
};
vm.confirmVsyaRf();
vm.form.name = 'Тест';
vm.form.signal_identifier = 'test.ru';
await vm.submit();
await flushPromises();
// Диалог перегрузки должен быть открыт
const overload = wrapper.find('[data-testid="overload-dialog"]');
expect(overload.exists()).toBe(true);
// Payload пробрасывается — показывается сумма пополнения
expect(overload.text()).toContain('1050.00');
// saved должен был эмититься (форма считается созданной)
expect(wrapper.emitted('saved')).toBeTruthy();
});
it('при launch.deferred=0 диалога перегрузки нет, эмитится saved', async () => {
(apiClient.post as ReturnType).mockResolvedValueOnce({
data: {
data: { id: 43, name: 'Тест2' },
launch: {
launched: 1,
deferred: 0,
balance: null,
},
},
});
const wrapper = factory();
await flushPromises();
const vm = wrapper.vm as unknown as {
confirmVsyaRf: () => void;
form: { name: string; signal_identifier: string };
submit: () => Promise;
};
vm.confirmVsyaRf();
vm.form.name = 'Тест2';
vm.form.signal_identifier = 'test2.ru';
await vm.submit();
await flushPromises();
// Диалога перегрузки быть не должно
const overload = wrapper.find('[data-testid="overload-dialog"]');
expect(overload.exists()).toBe(false);
// saved эмититься должен
expect(wrapper.emitted('saved')).toBeTruthy();
});
it('при launch.deferred=1 баннер не показывает ошибку в форме', async () => {
(apiClient.post as ReturnType).mockResolvedValueOnce({
data: {
data: { id: 44 },
launch: {
launched: 0,
deferred: 1,
balance: {
current_balance_rub: '0.00',
current_capacity_leads: 0,
would_be_required_leads: 50,
deficit_leads: 50,
topup_rub: '1500.00',
},
},
},
});
const wrapper = factory();
await flushPromises();
const vm = wrapper.vm as unknown as {
confirmVsyaRf: () => void;
form: { name: string; signal_identifier: string };
submit: () => Promise;
};
vm.confirmVsyaRf();
vm.form.name = 'Тест3';
vm.form.signal_identifier = 'test3.ru';
await vm.submit();
await flushPromises();
// В форме нет общей ошибки
const errorAlert = wrapper.find('.dialog-stub .v-alert--type-error');
expect(errorAlert.exists()).toBe(false);
});
it('update (edit) 409 balance_insufficient читает payload из err.response.data.balance', async () => {
// В edit-режиме PATCH всё ещё может вернуть 409 — payload вложен в .balance
(apiClient.patch as ReturnType).mockRejectedValueOnce({
response: {
status: 409,
data: {
error: 'balance_insufficient',
balance: {
current_balance_rub: '200.00',
current_capacity_leads: 6,
would_be_required_leads: 80,
deficit_leads: 74,
topup_rub: '2220.00',
},
},
},
});
const project = {
id: 5,
name: 'Проект',
signal_type: 'site' as const,
signal_identifier: 'edit.ru',
daily_limit_target: 10,
delivered_today: 0,
is_active: true,
regions: [77],
delivery_days_mask: 127,
sync_status: 'ok' as const,
region_mask: 0,
region_mode: 'include',
} as unknown as Project;
const wrapper = factory({ modelValue: true, mode: 'edit', project });
await flushPromises();
const vm = wrapper.vm as unknown as { submit: () => Promise };
await vm.submit();
await flushPromises();
const overload = wrapper.find('[data-testid="overload-dialog"]');
expect(overload.exists()).toBe(true);
// Проверяем что payload из .balance дошёл (сумма 2220)
expect(overload.text()).toContain('2220');
});
});