import { describe, it, expect } from 'vitest'; import { mount } from '@vue/test-utils'; import { createVuetify } from 'vuetify'; import { createPinia, setActivePinia } from 'pinia'; import KanbanView from '../../resources/js/views/KanbanView.vue'; import { LEAD_STATUSES } from '../../resources/js/composables/leadStatuses'; describe('KanbanView.vue', () => { // KanbanView содержит DealDetailDrawer (v-navigation-drawer), который требует // injected layout от v-app — оборачиваем в v-app для теста. // KanbanView содержит DealDetailDrawer (v-navigation-drawer) — stub'им, // т.к. layout-injection недоступна в Vitest. Drawer тестируется отдельно. const factory = () => { setActivePinia(createPinia()); return mount(KanbanView, { global: { plugins: [createVuetify()], stubs: { DealDetailDrawer: true, NewDealDialog: true }, }, }); }; it('монтируется и содержит заголовок «Канбан»', () => { const wrapper = factory(); expect(wrapper.find('h1').text()).toBe('Канбан'); }); it('рендерит ровно 14 KanbanColumn (по числу lead_statuses)', () => { const wrapper = factory(); const cols = wrapper.findAllComponents({ name: 'KanbanColumn' }); expect(cols).toHaveLength(LEAD_STATUSES.length); expect(cols).toHaveLength(14); }); it('каждая колонка получает соответствующий статус', () => { const wrapper = factory(); const cols = wrapper.findAllComponents({ name: 'KanbanColumn' }); cols.forEach((col, i) => { expect(col.props('status').slug).toBe(LEAD_STATUSES[i].slug); }); }); it('содержит page-stats с числом статусов и сделок', () => { const wrapper = factory(); const text = wrapper.text(); expect(text).toContain('14'); expect(text).toContain('статусов'); expect(text).toContain('сделок'); }); it('содержит кнопку «Новая сделка»', () => { const wrapper = factory(); expect(wrapper.text()).toContain('Новая сделка'); }); it('содержит подсказку про перетаскивание (DnD активен)', () => { const wrapper = factory(); expect(wrapper.text()).toMatch(/[Пп]еретаскивание/); }); it('кнопка «Новая сделка» открывает NewDealDialog', async () => { const wrapper = factory(); const vm = wrapper.vm as unknown as { newDealOpen: boolean }; expect(vm.newDealOpen).toBe(false); await wrapper.find('[data-testid="new-deal-btn"]').trigger('click'); await wrapper.vm.$nextTick(); expect(vm.newDealOpen).toBe(true); }); it('onDealCreated кладёт сделку в правильную колонку по statusSlug', async () => { const wrapper = factory(); const vm = wrapper.vm as unknown as { dealsByStatus: Record>; onDealCreated: (deal: Record) => void; totalDeals: number; }; const beforeNew = vm.dealsByStatus.new.length; const beforeTotal = vm.totalDeals; // Передаём полную форму deal — Kanban-карточка ожидает manager/cost/etc. vm.onDealCreated({ id: 999, name: 'Тест', phone: '+7 (999) 000-00-00', statusSlug: 'new', project: 'Окна Москва', manager: { initials: 'Т', name: 'Тест' }, cost: 100, receivedMinutesAgo: 0, }); await wrapper.vm.$nextTick(); expect(vm.dealsByStatus.new.length).toBe(beforeNew + 1); expect(vm.dealsByStatus.new[0].id).toBe(999); expect(vm.totalDeals).toBe(beforeTotal + 1); }); it('обновляет statusSlug сделки при drop в новую колонку (event.added)', async () => { const wrapper = factory(); const cols = wrapper.findAllComponents({ name: 'KanbanColumn' }); // Берём сделку из первой колонки (new) и эмулируем «added» в paid-колонке. const newCol = cols[0]; // new — sortOrder=1 const paidCol = cols.find((c) => c.props('status').slug === 'paid')!; const dealToMove = (newCol.props('deals') as { id: number; statusSlug: string }[])[0]; // Эмуляция события vuedraggable@change → KanbanView.onColumnChange. await paidCol.vm.$emit('change', { added: { element: dealToMove, newIndex: 0 }, }); await wrapper.vm.$nextTick(); // statusSlug сделки должен переключиться на 'paid'. expect(dealToMove.statusSlug).toBe('paid'); }); });