import { describe, it, expect, beforeEach, vi } from 'vitest'; import { mount, flushPromises } from '@vue/test-utils'; import { createVuetify } from 'vuetify'; import { createRouter, createMemoryHistory } from 'vue-router'; // Мокаем api/admin до import'а view (loadSettings вызывается на mount). vi.mock('../../resources/js/api/admin', () => ({ listSystemSettings: vi.fn(() => Promise.resolve([])), // default — пустой массив updateSystemSetting: vi.fn(), })); vi.mock('../../resources/js/api/client', () => ({ extractErrorMessage: vi.fn((_e, fb?: string) => fb ?? 'err'), extractValidationErrors: vi.fn(() => null), apiClient: {}, ensureCsrfCookie: vi.fn(), })); import * as adminApi from '../../resources/js/api/admin'; import AdminSystemView from '../../resources/js/views/admin/AdminSystemView.vue'; import { ADMIN_SYSTEM_SETTINGS } from '../../resources/js/composables/mockAdmin'; const mountView = async () => { const router = createRouter({ history: createMemoryHistory(), routes: [{ path: '/admin/system', component: AdminSystemView }], }); await router.push('/admin/system'); await router.isReady(); const wrapper = mount(AdminSystemView, { global: { plugins: [createVuetify(), router], stubs: { SystemSettingEditDialog: true }, }, }); await flushPromises(); return wrapper; }; describe('AdminSystemView.vue', () => { beforeEach(() => { vi.clearAllMocks(); // По умолчанию backend возвращает те же 7 mock-настроек — view replace // settingsState на ту же форму, тесты структуры остаются валидными. vi.mocked(adminApi.listSystemSettings).mockResolvedValue( ADMIN_SYSTEM_SETTINGS.map((s) => ({ key: s.key, value: s.value, type: s.type, description: s.description, updated_at: s.updated_at, updated_by: null, })), ); }); it('монтируется и содержит заголовок «Система»', async () => { const wrapper = await mountView(); expect(wrapper.text()).toContain('Система'); }); it('показывает информационный alert про edit-flow + audit-log', async () => { const wrapper = await mountView(); const text = wrapper.text(); expect(text).toContain('Edit-flow'); expect(text).toContain('saas_admin_audit_log'); }); it('перечисляет ключевые system_settings (rate-limit, retention, login_max_attempts)', async () => { const wrapper = await mountView(); const text = wrapper.text(); expect(text).toContain('webhook_rate_limit_rps'); expect(text).toContain('login_max_attempts'); expect(text).toContain('password_min_length'); expect(text).toContain('webhook_log_retention_days'); expect(text).toContain('maintenance_mode'); }); it('содержит type-chip для каждой строки (int/string/bool/json)', async () => { const wrapper = await mountView(); const text = wrapper.text(); expect(text).toContain('int'); expect(text).toContain('bool'); }); it('число строк settings = 7 (mock count)', async () => { const wrapper = await mountView(); const rows = wrapper.findAll('[data-testid="setting-row"]'); expect(rows.length).toBe(7); }); it('каждая строка имеет «Изменить» кнопку с уникальным data-testid', async () => { const wrapper = await mountView(); const editBtns = wrapper.findAll('[data-testid$="-btn"]').filter((b) => b.text().includes('Изменить')); expect(editBtns.length).toBe(7); }); it('click на Изменить открывает edit-dialog с правильным setting', async () => { const wrapper = await mountView(); const vm = wrapper.vm as unknown as { editOpen: boolean; editSetting: { key: string } | null; }; expect(vm.editOpen).toBe(false); await wrapper.find('[data-testid="edit-login_max_attempts-btn"]').trigger('click'); await wrapper.vm.$nextTick(); expect(vm.editOpen).toBe(true); expect(vm.editSetting?.key).toBe('login_max_attempts'); }); it('на mount вызывает listSystemSettings (реальный fetch с backend)', async () => { await mountView(); await flushPromises(); expect(adminApi.listSystemSettings).toHaveBeenCalledTimes(1); }); it('кнопка Обновить триггерит loadSettings', async () => { const wrapper = await mountView(); await flushPromises(); vi.mocked(adminApi.listSystemSettings).mockClear(); await wrapper.find('[data-testid="reload-btn"]').trigger('click'); await flushPromises(); expect(adminApi.listSystemSettings).toHaveBeenCalledTimes(1); }); it('при сетевой ошибке показывает warning-banner + settingsState пустой', async () => { vi.mocked(adminApi.listSystemSettings).mockRejectedValueOnce(new Error('Network down')); const wrapper = await mountView(); await flushPromises(); const banner = wrapper.find('[data-testid="fetch-error-alert"]'); expect(banner.exists()).toBe(true); // Пустой при ошибке — без mock-fallback const rows = wrapper.findAll('[data-testid="setting-row"]'); expect(rows.length).toBe(0); }); it('onSettingUpdated обновляет value и updated_at в settingsState', async () => { const wrapper = await mountView(); const vm = wrapper.vm as unknown as { settingsState: Array<{ key: string; value: string; updated_at: string }>; onSettingUpdated: (p: { key: string; value: string; updated_at: string }) => void; }; vm.onSettingUpdated({ key: 'login_max_attempts', value: '7', updated_at: '2026-05-09T11:30:00', }); await wrapper.vm.$nextTick(); const row = vm.settingsState.find((s) => s.key === 'login_max_attempts'); expect(row?.value).toBe('7'); expect(row?.updated_at).toBe('2026-05-09T11:30:00'); }); it('G9: edit-кнопки имеют aria-label с ключом настройки', async () => { const wrapper = await mountView(); const editBtns = wrapper.findAll('[data-testid^="edit-"]'); expect(editBtns.length).toBeGreaterThan(0); for (const btn of editBtns) { const label = btn.attributes('aria-label') ?? ''; expect(label).toMatch(/^Изменить настройку .+/); } }); });