import { describe, it, expect, beforeEach, vi } from 'vitest'; import { mount, flushPromises } from '@vue/test-utils'; import { createVuetify } from 'vuetify'; vi.mock('../../resources/js/api/deals', () => ({ createDeal: vi.fn(), listProjects: vi.fn(() => Promise.resolve([])), listManagers: vi.fn(() => Promise.resolve([])), })); vi.mock('../../resources/js/api/client', () => ({ extractErrorMessage: vi.fn((_e, fb?: string) => fb ?? 'err'), extractValidationErrors: vi.fn(() => null), apiClient: {}, ensureCsrfCookie: vi.fn(), })); import * as dealsApi from '../../resources/js/api/deals'; import NewDealDialog from '../../resources/js/components/deals/NewDealDialog.vue'; const factory = (props: { modelValue: boolean; presetStatus?: string; tenantId?: number } = { modelValue: true }) => mount(NewDealDialog, { props, global: { plugins: [createVuetify()], stubs: { VDialog: { template: '
', props: ['modelValue'], }, }, }, }); beforeEach(() => { vi.clearAllMocks(); }); describe('NewDealDialog.vue', () => { it('не рендерит content при modelValue=false', () => { const wrapper = factory({ modelValue: false }); expect(wrapper.find('.dialog-stub').exists()).toBe(false); }); it('рендерит 6 полей: name/phone/project/manager/cost/status + кнопки', () => { const wrapper = factory(); const text = wrapper.text(); expect(text).toContain('Новая сделка'); ['field-name', 'field-phone', 'field-project', 'field-manager', 'field-cost', 'field-status'].forEach((id) => { expect(wrapper.find(`[data-testid="${id}"]`).exists()).toBe(true); }); expect(wrapper.find('[data-testid="submit-btn"]').exists()).toBe(true); expect(wrapper.find('[data-testid="cancel-btn"]').exists()).toBe(true); }); it('submit без данных не emits created — только validation errors', async () => { const wrapper = factory(); await wrapper.find('[data-testid="submit-btn"]').trigger('click'); await flushPromises(); expect(wrapper.emitted('created')).toBeUndefined(); // Errors отображаются (хотя бы для name/phone/project/manager) const text = wrapper.text(); expect(text).toContain('Имя обязательно'); expect(text).toContain('Телефон обязателен'); }); it('submit с валидными данными emits created с правильным deal + закрывает dialog', async () => { const wrapper = factory(); const vm = wrapper.vm as unknown as { name: string; phone: string; project: string; manager: { initials: string; name: string }; cost: number; statusSlug: string; }; // Заполняем через прямой доступ к ref (компонент использует script setup) vm.name = 'Тест Тестов'; vm.phone = '+7 (999) 123-45-67'; vm.project = 'Окна Москва'; vm.manager = { initials: 'ИП', name: 'Иван П.' }; vm.cost = 1500; vm.statusSlug = 'new'; await flushPromises(); await wrapper.find('[data-testid="submit-btn"]').trigger('click'); await flushPromises(); const created = wrapper.emitted('created'); expect(created).toBeDefined(); const deal = (created![0] as unknown[])[0] as { name: string; phone: string; statusSlug: string; cost: number; project: string; }; expect(deal.name).toBe('Тест Тестов'); expect(deal.phone).toBe('+7 (999) 123-45-67'); expect(deal.cost).toBe(1500); expect(deal.project).toBe('Окна Москва'); expect(deal.statusSlug).toBe('new'); const close = wrapper.emitted('update:modelValue'); expect(close).toBeDefined(); expect(close![0]).toEqual([false]); }); it('phone < 10 цифр → validation error «Минимум 10 цифр»', async () => { const wrapper = factory(); const vm = wrapper.vm as unknown as { name: string; phone: string; project: string; manager: { initials: string; name: string }; }; vm.name = 'X'; vm.phone = '123'; vm.project = 'Окна Москва'; vm.manager = { initials: 'X', name: 'X' }; await flushPromises(); await wrapper.find('[data-testid="submit-btn"]').trigger('click'); await flushPromises(); expect(wrapper.emitted('created')).toBeUndefined(); expect(wrapper.text()).toContain('Минимум 10 цифр'); }); it('presetStatus → statusSlug дефолтит на пресет (для KanbanView)', async () => { const wrapper = factory({ modelValue: true, presetStatus: 'paid' }); await flushPromises(); const vm = wrapper.vm as unknown as { statusSlug: string }; expect(vm.statusSlug).toBe('paid'); }); it('без tenantId — submit НЕ вызывает API (local-only mode)', async () => { const wrapper = factory({ modelValue: true }); // tenantId не передан const vm = wrapper.vm as unknown as { name: string; phone: string; project: string; manager: { initials: string; name: string }; statusSlug: string; }; vm.name = 'X'; vm.phone = '+7 (999) 000-00-00'; vm.project = 'Окна Москва'; vm.manager = { initials: 'X', name: 'X' }; vm.statusSlug = 'new'; await flushPromises(); await wrapper.find('[data-testid="submit-btn"]').trigger('click'); await flushPromises(); expect(dealsApi.createDeal).not.toHaveBeenCalled(); expect(wrapper.emitted('created')).toBeDefined(); }); it('с tenantId + успешный backend — emits deal с backend-id', async () => { vi.mocked(dealsApi.createDeal).mockResolvedValue({ id: 4242, tenant_id: 1, project_id: 7, phone: '+7 (999) 000-00-00', status: 'new', contact_name: 'X', manager_id: null, received_at: '2026-05-09T12:00:00Z', }); const wrapper = factory({ modelValue: true, tenantId: 1 }); const vm = wrapper.vm as unknown as { name: string; phone: string; project: string; manager: { initials: string; name: string }; statusSlug: string; }; vm.name = 'X'; vm.phone = '+7 (999) 000-00-00'; vm.project = 'Окна Москва'; vm.manager = { initials: 'X', name: 'X' }; vm.statusSlug = 'new'; await flushPromises(); await wrapper.find('[data-testid="submit-btn"]').trigger('click'); await flushPromises(); expect(dealsApi.createDeal).toHaveBeenCalledWith({ tenant_id: 1, project_name: 'Окна Москва', phone: '+7 (999) 000-00-00', contact_name: 'X', status: 'new', }); const emitted = wrapper.emitted('created'); expect(emitted).toBeDefined(); const deal = (emitted![0] as unknown[])[0] as { id: number }; expect(deal.id).toBe(4242); // backend-id, не local nextId() }); it('с tenantId — loadLookups вызывает listManagers + listProjects на open', async () => { vi.mocked(dealsApi.listProjects).mockResolvedValue([ { id: 7, name: 'Окна Москва', tag: null, type: 'webhook' }, ]); vi.mocked(dealsApi.listManagers).mockResolvedValue([ { id: 42, email: 'iv@ex.ru', first_name: 'Иван', last_name: 'Петров', name: 'Иван П.', initials: 'ИП' }, ]); const wrapper = factory({ modelValue: true, tenantId: 1 }); await flushPromises(); expect(dealsApi.listProjects).toHaveBeenCalledWith(1); expect(dealsApi.listManagers).toHaveBeenCalledWith(1); // projectOptions / managerOptions заменены backend'ом const vm = wrapper.vm as unknown as { projectOptions: string[]; managerOptions: Array<{ name: string; initials: string }>; managerIdByName: Map; }; expect(vm.projectOptions).toEqual(['Окна Москва']); expect(vm.managerOptions).toEqual([{ name: 'Иван П.', initials: 'ИП' }]); expect(vm.managerIdByName.get('Иван П.')).toBe(42); }); it('submit с manager → передаёт backend manager_id из mapping', async () => { vi.mocked(dealsApi.listManagers).mockResolvedValue([ { id: 99, email: 'x@y.ru', first_name: 'X', last_name: 'Y', name: 'X Y.', initials: 'XY' }, ]); vi.mocked(dealsApi.createDeal).mockResolvedValue({ id: 1, tenant_id: 1, project_id: 1, phone: '+7 (999) 000-00-00', status: 'new', contact_name: 'Z', manager_id: 99, received_at: '2026-05-09T12:00:00Z', }); const wrapper = factory({ modelValue: true, tenantId: 1 }); await flushPromises(); const vm = wrapper.vm as unknown as { name: string; phone: string; project: string; manager: { initials: string; name: string }; statusSlug: string; }; vm.name = 'Z'; vm.phone = '+7 (999) 000-00-00'; vm.project = 'Окна Москва'; vm.manager = { initials: 'XY', name: 'X Y.' }; vm.statusSlug = 'new'; await flushPromises(); await wrapper.find('[data-testid="submit-btn"]').trigger('click'); await flushPromises(); expect(dealsApi.createDeal).toHaveBeenCalledWith( expect.objectContaining({ manager_id: 99, }), ); }); it('с tenantId + backend-error — fallback на local-id + warning + emit', async () => { vi.mocked(dealsApi.createDeal).mockRejectedValue(new Error('Network down')); const wrapper = factory({ modelValue: true, tenantId: 1 }); const vm = wrapper.vm as unknown as { name: string; phone: string; project: string; manager: { initials: string; name: string }; statusSlug: string; submitError: string | null; }; vm.name = 'X'; vm.phone = '+7 (999) 000-00-00'; vm.project = 'Окна Москва'; vm.manager = { initials: 'X', name: 'X' }; vm.statusSlug = 'new'; await flushPromises(); await wrapper.find('[data-testid="submit-btn"]').trigger('click'); await flushPromises(); // Warning показывается, deal эмитится с local-id, диалог остаётся открытым. expect(wrapper.find('[data-testid="submit-error-alert"]').exists()).toBe(true); expect(wrapper.emitted('created')).toBeDefined(); // dialog НЕ закрывается на error const closeEmits = wrapper.emitted('update:modelValue'); expect(closeEmits === undefined || !closeEmits.some((e) => e[0] === false)).toBe(true); }); });