Files
portal/app/tests/Frontend/AdminSystemView.spec.ts
T

163 lines
6.9 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, 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(/^Изменить настройку .+/);
}
});
});