/** * 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'); }); });