import { describe, it, expect, beforeEach, vi } from 'vitest';
import { mount, flushPromises } from '@vue/test-utils';
import { createVuetify } from 'vuetify';
vi.mock('../../resources/js/api/admin', () => ({
updateSystemSetting: vi.fn(),
listSystemSettings: vi.fn(),
}));
vi.mock('../../resources/js/api/client', () => ({
extractValidationErrors: vi.fn(() => null),
extractErrorMessage: vi.fn((_e, fb?: string) => fb ?? 'err'),
apiClient: {},
ensureCsrfCookie: vi.fn(),
}));
import * as adminApi from '../../resources/js/api/admin';
import SystemSettingEditDialog from '../../resources/js/components/admin/SystemSettingEditDialog.vue';
const sampleSetting: adminApi.SystemSetting = {
key: 'login_max_attempts',
value: '5',
type: 'int',
description: 'Макс. неудачных попыток входа в окне 15 минут',
updated_at: '2026-05-09T10:00:00',
updated_by: null,
};
const factory = (
props: { modelValue: boolean; setting: adminApi.SystemSetting | null } = {
modelValue: true,
setting: sampleSetting,
},
) =>
mount(SystemSettingEditDialog, {
props: { ...props, requestedBy: 1 },
global: {
plugins: [createVuetify()],
stubs: {
VDialog: {
template: '
',
props: ['modelValue'],
},
},
},
});
describe('SystemSettingEditDialog.vue', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('не рендерит content при modelValue=false', () => {
const wrapper = factory({ modelValue: false, setting: sampleSetting });
expect(wrapper.find('.dialog-stub').exists()).toBe(false);
});
it('step 1 — показывает key + type-chip + текущее значение + 2 input', () => {
const wrapper = factory();
const text = wrapper.text();
expect(text).toContain('login_max_attempts');
expect(text).toContain('int');
expect(text).toContain('Макс. неудачных попыток');
expect(wrapper.find('[data-testid="value-input"]').exists()).toBe(true);
expect(wrapper.find('[data-testid="reason-input"]').exists()).toBe(true);
expect(wrapper.find('[data-testid="next-btn"]').exists()).toBe(true);
});
it('newValue по умолчанию = текущее значение setting', () => {
const wrapper = factory();
const vm = wrapper.vm as unknown as { newValue: string };
expect(vm.newValue).toBe('5');
});
it('Далее без изменения значения → ошибка «совпадает»', async () => {
const wrapper = factory();
const vm = wrapper.vm as unknown as { reason: string; valueError: string | null; step: string };
vm.reason = 'A'.repeat(35);
await wrapper.vm.$nextTick();
await wrapper.find('[data-testid="next-btn"]').trigger('click');
await flushPromises();
expect(vm.valueError).toContain('совпадает');
expect(vm.step).toBe('edit');
});
it('Далее с reason < 30 chars → ошибка', async () => {
const wrapper = factory();
const vm = wrapper.vm as unknown as {
newValue: string;
reason: string;
reasonError: string | null;
step: string;
};
vm.newValue = '7';
vm.reason = 'короткое';
await wrapper.vm.$nextTick();
await wrapper.find('[data-testid="next-btn"]').trigger('click');
await flushPromises();
expect(vm.reasonError).toBe('Минимум 30 символов.');
expect(vm.step).toBe('edit');
});
it('Далее с валидными данными → step confirm + diff before/after', async () => {
const wrapper = factory();
const vm = wrapper.vm as unknown as { newValue: string; reason: string; step: string };
vm.newValue = '7';
vm.reason = 'Решение CTO от 09.05.2026: ослабляем лимит для UX тестирования.';
await wrapper.vm.$nextTick();
await wrapper.find('[data-testid="next-btn"]').trigger('click');
await flushPromises();
expect(vm.step).toBe('confirm');
const text = wrapper.text();
expect(text).toContain('Было');
expect(text).toContain('Станет');
// Reason отображается на confirm-step
expect(text).toContain('CTO от 09.05');
});
it('Применить → API + emit updated + step done', async () => {
vi.mocked(adminApi.updateSystemSetting).mockResolvedValue({
key: 'login_max_attempts',
value: '7',
previous_value: '5',
updated_at: '2026-05-09T11:00:00',
message: 'OK',
});
const wrapper = factory();
const vm = wrapper.vm as unknown as { newValue: string; reason: string; step: string };
vm.newValue = '7';
vm.reason = 'Решение CTO от 09.05.2026: ослабляем лимит для UX тестирования.';
await wrapper.vm.$nextTick();
await wrapper.find('[data-testid="next-btn"]').trigger('click');
await flushPromises();
await wrapper.find('[data-testid="submit-btn"]').trigger('click');
await flushPromises();
expect(adminApi.updateSystemSetting).toHaveBeenCalledWith('login_max_attempts', {
value: '7',
reason: expect.stringContaining('CTO'),
admin_user_id: 1,
});
expect(wrapper.emitted('updated')).toBeDefined();
expect(vm.step).toBe('done');
});
it('Назад с confirm возвращает на edit с сохранёнными данными', async () => {
const wrapper = factory();
const vm = wrapper.vm as unknown as { newValue: string; reason: string; step: string };
vm.newValue = '7';
vm.reason = 'Решение CTO от 09.05.2026: ослабляем лимит для UX тестирования.';
await wrapper.vm.$nextTick();
await wrapper.find('[data-testid="next-btn"]').trigger('click');
await flushPromises();
expect(vm.step).toBe('confirm');
await wrapper.find('[data-testid="back-btn"]').trigger('click');
await flushPromises();
expect(vm.step).toBe('edit');
expect(vm.newValue).toBe('7'); // сохранилось
});
});