import { describe, it, expect, beforeEach, vi } from 'vitest'; import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; import { MemoryRouter, Route, Routes } from 'react-router-dom'; import '@testing-library/jest-dom/vitest'; import { RouteGuard } from '../../app/guards/RouteGuard'; import { useDemoStore } from '../../stores/demo'; import { useThemeStore } from '../../stores/theme'; import { Modal } from '../../shared/ui/Modal'; import { exportDatabaseSnapshot, importDatabaseSnapshot, resetDatabase, putStoreItem, getAllFromStore, } from '../../services/storage/indexedDb'; import { metadataService } from '../../services/mock'; describe('Phase F: Accessibility, Security & Navigation Test Suite', () => { beforeEach(async () => { await resetDatabase(); useDemoStore.getState().resetDemoControls(); localStorage.clear(); document.documentElement.removeAttribute('data-theme'); }); // ---------------------------------------------------- // 1. RBAC Route Denial: Insufficient permissions -> 403 Forbidden // ---------------------------------------------------- describe('RBAC Route Denial', () => { it('denies Cashier role navigating to /finance and renders 403 Forbidden', () => { useDemoStore.getState().setCurrentRoleId('cashier'); render(
Секретные проводки
} />
); expect(screen.queryByTestId('finance-page')).not.toBeInTheDocument(); expect(screen.getByText(/403/i)).toBeInTheDocument(); expect(screen.getByText(/Доступ ограничен/i)).toBeInTheDocument(); expect(screen.getByText(/Кассир \/ Оператор/i)).toBeInTheDocument(); }); it('denies Cashier role navigating to /settings and renders 403 Forbidden', () => { useDemoStore.getState().setCurrentRoleId('cashier'); render(
Системные настройки
} />
); expect(screen.queryByTestId('settings-page')).not.toBeInTheDocument(); expect(screen.getByText(/403/i)).toBeInTheDocument(); expect(screen.getByText(/Доступ ограничен/i)).toBeInTheDocument(); }); it('denies Cook role navigating to /crm and renders 403 Forbidden', () => { useDemoStore.getState().setCurrentRoleId('cook'); render(
Клиентская база
} />
); expect(screen.queryByTestId('crm-page')).not.toBeInTheDocument(); expect(screen.getByText(/403/i)).toBeInTheDocument(); expect(screen.getByText(/Повар \/ Шеф-повар/i)).toBeInTheDocument(); }); it('allows Owner role full access to all protected modules', () => { useDemoStore.getState().setCurrentRoleId('owner'); render(
Доступные финансы
} />
); expect(screen.getByTestId('finance-page')).toBeInTheDocument(); expect(screen.queryByText(/403/)).not.toBeInTheDocument(); }); }); // ---------------------------------------------------- // 2. Smoke Test for all registered routes // ---------------------------------------------------- describe('Smoke Navigation Test for all Registered Routes', () => { const allRoutes = [ { path: '/', module: 'dashboard', name: 'Главная панель' }, { path: '/operations', module: 'operations', name: 'Операции' }, { path: '/crm', module: 'crm', name: 'CRM' }, { path: '/sales', module: 'sales', name: 'Продажи' }, { path: '/inventory', module: 'inventory', name: 'Склад' }, { path: '/finance', module: 'finance', name: 'Финансы' }, { path: '/employees', module: 'employees', name: 'Персонал' }, { path: '/communications', module: 'communications', name: 'Коммуникации' }, { path: '/helpdesk', module: 'helpdesk', name: 'Служба поддержки' }, { path: '/tasks', module: 'tasks', name: 'Задачи' }, { path: '/cms', module: 'cms', name: 'CMS' }, { path: '/marketing', module: 'marketing', name: 'Маркетинг' }, { path: '/analytics', module: 'analytics', name: 'Аналитика' }, { path: '/automation', module: 'automation', name: 'Автоматизация' }, { path: '/ai', module: 'ai', name: 'AI Center' }, { path: '/apps', module: 'apps', name: 'Приложения' }, { path: '/settings', module: 'settings', name: 'Настройки' }, ]; it.each(allRoutes)('mounts and renders $name at $path without crashing', ({ path, module, name }) => { useDemoStore.getState().setCurrentRoleId('owner'); const { container } = render(
Элемент страницы: {name}
} />
); expect(screen.getByTestId(`page-${module}`)).toBeInTheDocument(); expect(screen.getByText(`Элемент страницы: ${name}`)).toBeInTheDocument(); expect(container).toBeInTheDocument(); }); it('resolves custom entity route dynamically', async () => { await metadataService.createEntity({ code: 'contract', name: 'Договор', pluralName: 'Договоры', category: 'commerce', isSystem: false, maturity: 'target_prototype', primaryFieldKey: 'title', fields: [], }); render( Модуль кастомной сущности} /> ); expect(screen.getByTestId('custom-entity-page')).toBeInTheDocument(); }); }); // ---------------------------------------------------- // 3. Modal Focus Trap & Keyboard Navigation // ---------------------------------------------------- describe('Modal Focus Trap & Keyboard Navigation', () => { it('sets initial focus to first focusable element when opened', async () => { render( ); // Verify dialog is rendered with correct ARIA attributes const dialog = screen.getByRole('dialog'); expect(dialog).toBeInTheDocument(); expect(dialog).toHaveAttribute('aria-modal', 'true'); }); it('invokes onClose callback when Escape key is pressed', () => { const handleClose = vi.fn(); render(
Контент модального окна
); fireEvent.keyDown(window, { key: 'Escape' }); expect(handleClose).toHaveBeenCalledTimes(1); }); it('handles close button click via accessible button', () => { const handleClose = vi.fn(); render(
Контент
); const closeBtn = screen.getByLabelText('Закрыть диалоговое окно'); expect(closeBtn).toBeInTheDocument(); fireEvent.click(closeBtn); expect(handleClose).toHaveBeenCalledTimes(1); }); }); // ---------------------------------------------------- // 4. Theme Persistence & prefers-reduced-motion // ---------------------------------------------------- describe('Theme Persistence & Motion Accessibility', () => { it('persists theme selection across soft-light, light, and dark modes', () => { const { setTheme } = useThemeStore.getState(); setTheme('soft-light'); expect(localStorage.getItem('krona_theme_mode')).toBe('soft-light'); expect(document.documentElement.getAttribute('data-theme')).toBe('soft-light'); setTheme('light'); expect(localStorage.getItem('krona_theme_mode')).toBe('light'); expect(document.documentElement.getAttribute('data-theme')).toBe('light'); setTheme('dark'); expect(localStorage.getItem('krona_theme_mode')).toBe('dark'); expect(document.documentElement.getAttribute('data-theme')).toBe('dark'); }); it('respects prefers-reduced-motion media query environment', () => { const mediaQueryList = window.matchMedia('(prefers-reduced-motion: reduce)'); expect(mediaQueryList).toBeDefined(); }); }); // ---------------------------------------------------- // 5. Snapshot Export, Seed Reset & Import Round-Trip // ---------------------------------------------------- describe('Database Snapshot Round-Trip', () => { it('performs full export, reset, and restore cycle with data integrity', async () => { // 1. Seed specific test records await putStoreItem('records_data', { id: 'deal_special_001', entityType: 'deal', title: 'Сделка с рестораном «Небо»', amount: { minor: 15000000, currency: 'RUB' }, }); await putStoreItem('settings', { key: 'company_name', value: 'Крона Премиум' }); // 2. Export snapshot const snapshot = await exportDatabaseSnapshot(); expect(snapshot.stores.records_data.length).toBeGreaterThan(0); expect(snapshot.stores.settings.length).toBeGreaterThan(0); expect(snapshot.metadata.totalRecords).toBeGreaterThan(0); // 3. Reset database await resetDatabase(); const recordsAfterReset = await getAllFromStore('records_data'); expect(recordsAfterReset).toHaveLength(0); // 4. Import snapshot back await importDatabaseSnapshot(snapshot); // 5. Verify restored data const recordsRestored = await getAllFromStore<{ id: string; title: string }>('records_data'); expect(recordsRestored.some((r) => r.id === 'deal_special_001')).toBe(true); const settingsRestored = await getAllFromStore<{ key: string; value: string }>('settings'); expect(settingsRestored.some((s) => s.key === 'company_name' && s.value === 'Крона Премиум')).toBe(true); }); }); });