Files
portal/app/tests/Frontend/AppLayout.spec.ts
T
Дмитрий 5c8ad2738a feat(layout): dark topbar + sidebar cleanup + DevIndexBadge moved below
Sidebar: убраны Менеджеры/Напоминания; Работа в порядке
Проекты/Сделки/Канбан/Дашборд; Команда — только Настройки;
снят useRemindersStore (был только под reminders badge).

Topbar: тёмный фон linear-gradient(noir → #04261E) совпадающий
с sidebar #1271; убран breadcrumb «Рабочая область»;
v-toolbar__content padding-left:240 (не уходит под sidebar).

DevIndexBadge: top:64 (ниже топбара, не перекрывает user-chip).

Vitest AppLayout 15/15 PASS.

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

176 lines
7.1 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 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;
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('Напоминания');
});
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();
});
});