import { describe, it, expect, vi } from 'vitest'; import { mount } from '@vue/test-utils'; import { createPinia, setActivePinia } from 'pinia'; import { createVuetify } from 'vuetify'; import { createRouter, createMemoryHistory } from 'vue-router'; // Мокаем api/notifications до import'а AppLayout (использует store, который импортит api). vi.mock('../../resources/js/api/notifications', () => ({ listNotifications: vi.fn().mockResolvedValue({ items: [], unread_count: 0, total: 0 }), markNotificationRead: vi.fn(), markAllNotificationsRead: vi.fn(), deleteNotification: vi.fn(), })); vi.mock('../../resources/js/api/reminders', () => ({ listReminders: vi.fn().mockResolvedValue({ items: [], counts: { active: 0, today: 0, upcoming: 0, overdue: 0 }, }), createReminder: vi.fn(), updateReminder: vi.fn(), completeReminder: vi.fn(), deleteReminder: vi.fn(), })); import * as notificationsApi from '../../resources/js/api/notifications'; import AppLayout from '../../resources/js/layouts/AppLayout.vue'; import { useAuthStore } from '../../resources/js/stores/auth'; import { useNotificationsStore } from '../../resources/js/stores/notifications'; import { useDealsCountStore } from '../../resources/js/stores/dealsCount'; import type { AuthUser } from '../../resources/js/api/auth'; const mockUser: AuthUser = { id: 1, email: 'ivan.petrov@example.ru', first_name: 'Иван', last_name: 'Петров', tenant_id: 1, totp_enabled: false, last_login_at: null, }; // AppLayout содержит sidebar (6 nav-items в 3 группах) + topbar (crumb/search/user) + RouterView. const mountAppLayout = async (path = '/dashboard', user: AuthUser | null = mockUser) => { setActivePinia(createPinia()); const auth = useAuthStore(); auth.user = user; // B2: init deals count so badge renders (replaces hardcoded 247 in AppSidebar). useDealsCountStore().count = 247; const router = createRouter({ history: createMemoryHistory(), routes: [ { path: '/dashboard', component: { template: '
dashboard
' } }, { path: '/deals', component: { template: '
deals
' } }, { path: '/kanban', component: { template: '
kanban
' } }, { path: '/projects', component: { template: '
projects
' } }, { path: '/billing', component: { template: '
billing
' } }, { path: '/reports', component: { template: '
reports
' } }, { path: '/settings', component: { template: '
settings
' } }, ], }); await router.push(path); await router.isReady(); return mount(AppLayout, { global: { plugins: [createVuetify(), router] }, }); }; describe('AppLayout.vue', () => { it('монтируется без ошибок', async () => { const wrapper = await mountAppLayout(); expect(wrapper.exists()).toBe(true); }); it('содержит брендовый блок «Лидерра.» в sidebar', async () => { const wrapper = await mountAppLayout(); expect(wrapper.text()).toContain('Лидерра'); }); it('содержит 3 nav-группы: Работа, Финансы, Команда', async () => { const wrapper = await mountAppLayout(); const text = wrapper.text(); expect(text).toContain('Работа'); expect(text).toContain('Финансы'); expect(text).toContain('Команда'); }); it('содержит все 6 nav-пунктов (Менеджеры+Напоминания убраны по требованию заказчика)', async () => { const wrapper = await mountAppLayout(); const text = wrapper.text(); ['Проекты', 'Сделки', 'Канбан', 'Дашборд', 'Биллинг', 'Отчёты', 'Настройки'].forEach((label) => expect(text).toContain(label), ); expect(text).not.toContain('Менеджеры'); expect(text).not.toContain('Напоминания'); }); it('показывает счётчики только у пунктов с count', async () => { const wrapper = await mountAppLayout(); const text = wrapper.text(); expect(text).toContain('247'); // Сделки (mock) }); it('breadcrumb показывает текущую страницу', async () => { const wrapper = await mountAppLayout('/dashboard'); expect(wrapper.text()).toContain('Дашборд'); }); it('user-chip показывает initials и shortName из store user', async () => { const wrapper = await mountAppLayout(); const text = wrapper.text(); expect(text).toContain('ИП'); // initials Иван Петров expect(text).toContain('Иван П.'); // shortName }); it('при null user (гость) показывает «?» и «Гость»', async () => { const wrapper = await mountAppLayout('/dashboard', null); const text = wrapper.text(); expect(text).toContain('Гость'); expect(text).toContain('?'); }); it('при отсутствии first_name fallback на email', async () => { const wrapper = await mountAppLayout('/dashboard', { ...mockUser, first_name: null, last_name: null, }); expect(wrapper.text()).toContain('ivan.petrov@example.ru'); }); it('bell-icon кнопка существует', async () => { const wrapper = await mountAppLayout(); const bellBtn = wrapper.find('[data-testid="notifications-btn"]'); expect(bellBtn.exists()).toBe(true); }); it('pip скрыт когда unreadCount=0 (default state)', async () => { const wrapper = await mountAppLayout(); await wrapper.vm.$nextTick(); const pip = wrapper.find('[data-testid="notifications-pip"]'); expect(pip.exists()).toBe(false); }); it('pip показывает unreadCount когда > 0', async () => { const wrapper = await mountAppLayout(); const store = useNotificationsStore(); store.unreadCount = 5; await wrapper.vm.$nextTick(); const pip = wrapper.find('[data-testid="notifications-pip"]'); expect(pip.exists()).toBe(true); expect(pip.text()).toBe('5'); }); it('pip показывает «99+» когда unreadCount > 99', async () => { const wrapper = await mountAppLayout(); const store = useNotificationsStore(); store.unreadCount = 142; await wrapper.vm.$nextTick(); const pip = wrapper.find('[data-testid="notifications-pip"]'); expect(pip.text()).toBe('99+'); }); it('listNotifications вызывается на mount при наличии user', async () => { await mountAppLayout('/dashboard', mockUser); expect(notificationsApi.listNotifications).toHaveBeenCalled(); }); it('listNotifications НЕ вызывается на mount без user', async () => { vi.mocked(notificationsApi.listNotifications).mockClear(); await mountAppLayout('/dashboard', null); expect(notificationsApi.listNotifications).not.toHaveBeenCalled(); }); });