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);
});
});
});