2026-05-09 08:59:17 +03:00
|
|
|
|
import { describe, it, expect, beforeEach } from 'vitest';
|
2026-05-08 18:29:11 +03:00
|
|
|
|
import { mount } from '@vue/test-utils';
|
|
|
|
|
|
import { createVuetify } from 'vuetify';
|
2026-05-09 08:59:17 +03:00
|
|
|
|
import { createPinia, setActivePinia } from 'pinia';
|
2026-05-08 18:29:11 +03:00
|
|
|
|
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';
|
|
|
|
|
|
|
2026-05-09 08:59:17 +03:00
|
|
|
|
beforeEach(() => {
|
|
|
|
|
|
setActivePinia(createPinia());
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-05-08 18:29:11 +03:00
|
|
|
|
// DealDetailDrawer использует v-navigation-drawer, который требует layout-
|
|
|
|
|
|
// контекст от v-app/v-layout. В Vitest auto-import недоступен — stub'им
|
|
|
|
|
|
// v-navigation-drawer как passthrough div чтобы slot-content рендерился
|
|
|
|
|
|
// и был доступен для assertion.
|
|
|
|
|
|
|
|
|
|
|
|
describe('DealDetailDrawer.vue', () => {
|
|
|
|
|
|
const factory = (props: { open: boolean; deal: (typeof MOCK_DEALS)[number] | null }) =>
|
|
|
|
|
|
mount(DealDetailDrawer, {
|
|
|
|
|
|
props,
|
|
|
|
|
|
global: {
|
|
|
|
|
|
plugins: [createVuetify()],
|
|
|
|
|
|
stubs: {
|
|
|
|
|
|
VNavigationDrawer: {
|
|
|
|
|
|
template: '<div class="drawer-stub" v-if="modelValue"><slot /></div>',
|
|
|
|
|
|
props: ['modelValue'],
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const sampleDeal = MOCK_DEALS[0]; // Анна Соколова
|
|
|
|
|
|
|
|
|
|
|
|
it('не рендерит контент когда open=false', () => {
|
|
|
|
|
|
const wrapper = factory({ open: false, deal: sampleDeal });
|
|
|
|
|
|
expect(wrapper.find('.drawer-stub').exists()).toBe(false);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('не рендерит контент когда deal=null (даже при open=true)', () => {
|
|
|
|
|
|
const wrapper = factory({ open: true, deal: null });
|
|
|
|
|
|
// Drawer открыт, но deal нет — content внутри v-if не рендерится.
|
|
|
|
|
|
const stub = wrapper.find('.drawer-stub');
|
|
|
|
|
|
if (stub.exists()) {
|
|
|
|
|
|
// Нет hero/section элементов внутри.
|
|
|
|
|
|
expect(wrapper.find('.hero').exists()).toBe(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('рендерит hero с именем сделки и id', () => {
|
|
|
|
|
|
const wrapper = factory({ open: true, deal: sampleDeal });
|
|
|
|
|
|
const text = wrapper.text();
|
|
|
|
|
|
expect(text).toContain(sampleDeal.name);
|
|
|
|
|
|
expect(text).toContain(`#${sampleDeal.id}`);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('рендерит phone как кликабельную ссылку tel:', () => {
|
|
|
|
|
|
const wrapper = factory({ open: true, deal: sampleDeal });
|
|
|
|
|
|
const phoneLink = wrapper.find('.phone-link');
|
|
|
|
|
|
expect(phoneLink.exists()).toBe(true);
|
|
|
|
|
|
expect(phoneLink.attributes('href')).toMatch(/^tel:\+/);
|
|
|
|
|
|
expect(phoneLink.text()).toBe(sampleDeal.phone);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('рендерит status-chip с nameRu статуса сделки', () => {
|
|
|
|
|
|
const wrapper = factory({ open: true, deal: sampleDeal });
|
|
|
|
|
|
// sampleDeal.statusSlug='new' → 'Новые'.
|
|
|
|
|
|
expect(wrapper.text()).toContain('Новые');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('рендерит секцию параметров с проектом, стоимостью, менеджером', () => {
|
|
|
|
|
|
const wrapper = factory({ open: true, deal: sampleDeal });
|
|
|
|
|
|
const text = wrapper.text();
|
|
|
|
|
|
expect(text).toContain('Параметры');
|
|
|
|
|
|
expect(text).toContain(sampleDeal.project);
|
|
|
|
|
|
expect(text).toContain(sampleDeal.manager.name);
|
|
|
|
|
|
expect(text).toMatch(/1\s+850\s*₽/); // sampleDeal.cost = 1850
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('рендерит timeline с MOCK_EVENTS (6 событий)', () => {
|
|
|
|
|
|
const wrapper = factory({ open: true, deal: sampleDeal });
|
|
|
|
|
|
const items = wrapper.findAll('.timeline-item');
|
|
|
|
|
|
expect(items).toHaveLength(MOCK_EVENTS.length);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('emit-ит update:open=false при close-кнопке', async () => {
|
|
|
|
|
|
const wrapper = factory({ open: true, deal: sampleDeal });
|
|
|
|
|
|
// Vuetify v-btn рендерит как button. close-btn — единственный с aria-label.
|
|
|
|
|
|
const closeBtn = wrapper.find('button[aria-label="Закрыть панель"]');
|
|
|
|
|
|
if (closeBtn.exists()) {
|
|
|
|
|
|
await closeBtn.trigger('click');
|
|
|
|
|
|
expect(wrapper.emitted('update:open')).toBeTruthy();
|
|
|
|
|
|
expect(wrapper.emitted('update:open')?.[0]).toEqual([false]);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|