Files
portal/app/tests/Frontend/KanbanView.spec.ts
T

114 lines
5.1 KiB
TypeScript
Raw Normal View History

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<string, Array<{ id: number; statusSlug: string }>>;
onDealCreated: (deal: Record<string, unknown>) => 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');
});
});