import { describe, it, expect, vi, beforeEach } from 'vitest'; import { mount, flushPromises } from '@vue/test-utils'; import { createVuetify } from 'vuetify'; import { createPinia, setActivePinia } from 'pinia'; import DealDetailDrawer from '../../resources/js/components/deals/DealDetailDrawer.vue'; import { MOCK_DEALS } from '../../resources/js/composables/mockDeals'; import { MOCK_EVENTS } from '../../resources/js/composables/mockDealEvents'; import type { GetDealResponse, ApiDealEvent } from '../../resources/js/api/deals'; vi.mock('../../resources/js/api/deals', async (importOriginal) => { const orig = await importOriginal(); return { ...orig, getDeal: vi.fn(), updateDeal: vi.fn(), }; }); const dealsApi = await import('../../resources/js/api/deals'); beforeEach(() => { vi.clearAllMocks(); setActivePinia(createPinia()); }); const factory = (props: { open: boolean; tenantId?: number }) => mount(DealDetailDrawer, { props: { deal: MOCK_DEALS[0], ...props }, global: { plugins: [createVuetify()], stubs: { VNavigationDrawer: { template: '
', props: ['modelValue'], }, }, }, }); function makeApiEvent(overrides: Partial = {}): ApiDealEvent { return { id: 100, event: 'deal.created', context: { source: 'webhook' }, created_at: new Date(Date.now() - 30 * 60 * 1000).toISOString(), actor: null, ...overrides, }; } describe('DealDetailDrawer ↔ GET /api/deals/{id} integration', () => { it('БЕЗ tenantId — getDeal не вызывается, показываются MOCK_EVENTS', async () => { const wrapper = factory({ open: true }); await flushPromises(); expect(dealsApi.getDeal).not.toHaveBeenCalled(); const items = wrapper.findAll('.timeline-item'); expect(items).toHaveLength(MOCK_EVENTS.length); }); it('С tenantId — getDeal вызывается, events заменяются на API', async () => { const apiResponse: GetDealResponse = { deal: { id: MOCK_DEALS[0].id, tenant_id: 1, project_id: 5, project_name: 'Окна Москва', phone: '+7 (999) 100-00-01', contact_name: 'Anna', comment: null, status: 'new', manager_id: null, manager_name: null, manager_initials: null, received_at: new Date().toISOString(), assigned_at: null, }, events: [ makeApiEvent({ id: 1, event: 'deal.created', context: { source: 'webhook' } }), makeApiEvent({ id: 2, event: 'deal.status_changed', context: { from: 'new', to: 'paid' }, actor: { id: 1, name: 'Иван П.', initials: 'ИП' }, }), ], }; vi.mocked(dealsApi.getDeal).mockResolvedValueOnce(apiResponse); const wrapper = factory({ open: true, tenantId: 1 }); await flushPromises(); expect(dealsApi.getDeal).toHaveBeenCalledWith(MOCK_DEALS[0].id, 1); const items = wrapper.findAll('.timeline-item'); expect(items).toHaveLength(2); // status_changed event имеет detail "new → paid". expect(wrapper.text()).toContain('new → paid'); }); it('getDeal reject → eventsFetchError=true, alert виден, MOCK_EVENTS как fallback', async () => { vi.mocked(dealsApi.getDeal).mockRejectedValueOnce(new Error('500')); const wrapper = factory({ open: true, tenantId: 1 }); await flushPromises(); const vm = wrapper.vm as unknown as { eventsFetchError: boolean }; expect(vm.eventsFetchError).toBe(true); expect(wrapper.find('[data-testid="events-fetch-error-alert"]').exists()).toBe(true); // Fallback на MOCK_EVENTS. const items = wrapper.findAll('.timeline-item'); expect(items).toHaveLength(MOCK_EVENTS.length); }); it('open=false → getDeal не вызывается', async () => { factory({ open: false, tenantId: 1 }); await flushPromises(); expect(dealsApi.getDeal).not.toHaveBeenCalled(); }); it('saveComment вызывает updateDeal + toast success + reload events', async () => { const apiResponse: GetDealResponse = { deal: { id: MOCK_DEALS[0].id, tenant_id: 1, project_id: 5, project_name: 'Окна', phone: '+7 (999) 100-00-01', contact_name: 'Anna', comment: 'old', status: 'new', manager_id: null, manager_name: null, manager_initials: null, received_at: new Date().toISOString(), assigned_at: null, }, events: [], }; vi.mocked(dealsApi.getDeal).mockResolvedValue(apiResponse); vi.mocked(dealsApi.updateDeal).mockResolvedValueOnce({ ...apiResponse.deal, comment: 'новая заметка', }); const wrapper = factory({ open: true, tenantId: 1 }); await flushPromises(); const vm = wrapper.vm as unknown as { commentDraft: string; saveComment: () => Promise; commentToastOpen: boolean; commentToastText: string; commentSaveError: boolean; }; expect(vm.commentDraft).toBe('old'); // загружено из getDeal vm.commentDraft = 'новая заметка'; await vm.saveComment(); await flushPromises(); expect(dealsApi.updateDeal).toHaveBeenCalledWith(MOCK_DEALS[0].id, { tenant_id: 1, comment: 'новая заметка', }); expect(vm.commentToastOpen).toBe(true); expect(vm.commentToastText).toContain('сохранён'); expect(vm.commentSaveError).toBe(false); // reload events после save — getDeal вызвался ещё раз. expect(dealsApi.getDeal).toHaveBeenCalledTimes(2); }); it('saveComment reject → toast warning + commentSaveError=true', async () => { vi.mocked(dealsApi.getDeal).mockResolvedValueOnce({ deal: { id: MOCK_DEALS[0].id, tenant_id: 1, project_id: 5, project_name: null, phone: '+7 (999)', contact_name: null, comment: null, status: 'new', manager_id: null, manager_name: null, manager_initials: null, received_at: null, assigned_at: null, }, events: [], }); vi.mocked(dealsApi.updateDeal).mockRejectedValueOnce(new Error('500')); const wrapper = factory({ open: true, tenantId: 1 }); await flushPromises(); const vm = wrapper.vm as unknown as { commentDraft: string; saveComment: () => Promise; commentSaveError: boolean; commentToastText: string; }; vm.commentDraft = 'новый'; await vm.saveComment(); await flushPromises(); expect(vm.commentSaveError).toBe(true); expect(vm.commentToastText).toContain('Не удалось'); }); it('comment-section не показывается без tenantId (read-only mode)', async () => { const wrapper = factory({ open: true }); await flushPromises(); expect(wrapper.find('[data-testid="comment-section"]').exists()).toBe(false); }); });