Files
portal/app/tests/Frontend/AppLayout.spec.ts
T
Дмитрий 55a34af986 feat(deals): redesign groundwork — spec, plan, mockups + sidebar nav cleanup
Deals page redesign: design spec + implementation plan (Phase A page redesign,
Phase B 14->5 status funnel) + v8 HTML mockups (variants comparison + final).
AppSidebar: remove Импорт данных / Отчёты nav links (routes stay reachable by
direct URL); AppLayout.spec updated to 6 nav items. stylelint --fix on mockups;
cspell-words += deals-redesign terms.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 03:42:39 +03:00

181 lines
7.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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: '<div>dashboard</div>' } },
{ path: '/deals', component: { template: '<div>deals</div>' } },
{ path: '/kanban', component: { template: '<div>kanban</div>' } },
{ path: '/projects', component: { template: '<div>projects</div>' } },
{ path: '/billing', component: { template: '<div>billing</div>' } },
{ path: '/reports', component: { template: '<div>reports</div>' } },
{ path: '/settings', component: { template: '<div>settings</div>' } },
],
});
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('Напоминания');
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();
});
});