import { describe, it, expect, beforeEach, vi } from 'vitest'; import { mount } from '@vue/test-utils'; import { createPinia, setActivePinia } from 'pinia'; import { createVuetify } from 'vuetify'; vi.mock('../../resources/js/api/auth', () => ({ updateNotificationPreferences: vi.fn(), })); vi.mock('../../resources/js/api/client', () => ({ apiClient: {}, ensureCsrfCookie: vi.fn(), extractValidationErrors: vi.fn(() => null), extractErrorMessage: vi.fn(() => 'Произошла ошибка.'), extractRateLimitRetry: vi.fn(() => null), })); import * as authApi from '../../resources/js/api/auth'; import NotificationsTab from '../../resources/js/views/settings/NotificationsTab.vue'; import { useAuthStore } from '../../resources/js/stores/auth'; import type { AuthUser } from '../../resources/js/api/auth'; const mockUser: AuthUser = { id: 1, email: 'test@example.ru', first_name: 'Иван', last_name: 'Петров', tenant_id: 1, totp_enabled: false, last_login_at: null, notification_preferences: { new_lead: { inapp: true, push: true, email: false }, reminder: { inapp: true, push: true, email: true }, low_balance: { email: true }, zero_balance: { email: true }, topup_success: { email: true }, invoice_paid: { email: true }, new_device_login: { email: true }, marketing: { email: false }, }, sound_enabled: true, }; const factory = (user: AuthUser | null = mockUser) => { setActivePinia(createPinia()); const auth = useAuthStore(); auth.user = user; return mount(NotificationsTab, { global: { plugins: [createVuetify()] }, }); }; describe('NotificationsTab.vue (schema-aligned)', () => { beforeEach(() => vi.clearAllMocks()); it('содержит ровно 8 schema-aligned событий', () => { const wrapper = factory(); const text = wrapper.text(); [ 'Новый лид', 'Напоминание', 'Низкий баланс', 'Нулевой баланс', 'Пополнение успешно', 'Счёт оплачен', 'Новое устройство', 'Анонсы и промо', ].forEach((label) => expect(text).toContain(label)); }); it('содержит ровно 3 канала (inapp/push/email) — НЕ sms', () => { const wrapper = factory(); const text = wrapper.text(); expect(text).toContain('В приложении'); expect(text).toContain('Push'); expect(text).toContain('Email'); expect(text).not.toContain('SMS'); }); it('legacy-events отсутствуют (Дубликат / Webhook упал и т.д.)', () => { const wrapper = factory(); const text = wrapper.text(); ['Дубликат / антифрод', 'Срок напоминания', 'Webhook упал', 'Месячный отчёт', 'Назначен менеджер'].forEach( (legacy) => expect(text).not.toContain(legacy), ); }); it('читает prefs из auth.user при mount: new_lead.email=false / reminder.email=true', () => { const wrapper = factory(); const newLeadEmail = wrapper.find('[data-testid="pref-new_lead-email"] input'); const reminderEmail = wrapper.find('[data-testid="pref-reminder-email"] input'); expect((newLeadEmail.element as HTMLInputElement).checked).toBe(false); expect((reminderEmail.element as HTMLInputElement).checked).toBe(true); }); it('Сохранить disabled пока ничего не изменено', () => { const wrapper = factory(); const saveBtn = wrapper.find('[data-testid="save-btn"]'); expect(saveBtn.attributes('disabled')).toBeDefined(); }); it('после переключения checkbox Сохранить становится enabled', async () => { const wrapper = factory(); const checkbox = wrapper.find('[data-testid="pref-new_lead-email"] input'); await checkbox.setValue(true); const saveBtn = wrapper.find('[data-testid="save-btn"]'); expect(saveBtn.attributes('disabled')).toBeUndefined(); }); it('save() вызывает API и показывает success-alert', async () => { vi.mocked(authApi.updateNotificationPreferences).mockResolvedValue({ ...mockUser, notification_preferences: { ...mockUser.notification_preferences, new_lead: { inapp: true, push: true, email: true }, }, }); const wrapper = factory(); const checkbox = wrapper.find('[data-testid="pref-new_lead-email"] input'); await checkbox.setValue(true); await wrapper.find('[data-testid="save-btn"]').trigger('click'); await new Promise((resolve) => setTimeout(resolve, 0)); await wrapper.vm.$nextTick(); expect(authApi.updateNotificationPreferences).toHaveBeenCalled(); const callArg = vi.mocked(authApi.updateNotificationPreferences).mock.calls[0]![0]; expect(callArg.prefs?.new_lead?.email).toBe(true); expect(wrapper.find('[data-testid="notifications-save-success"]').exists()).toBe(true); }); it('save() при reject показывает error-alert', async () => { vi.mocked(authApi.updateNotificationPreferences).mockRejectedValue(new Error('500')); const wrapper = factory(); const checkbox = wrapper.find('[data-testid="pref-new_lead-email"] input'); await checkbox.setValue(true); await wrapper.find('[data-testid="save-btn"]').trigger('click'); await new Promise((resolve) => setTimeout(resolve, 0)); await wrapper.vm.$nextTick(); expect(wrapper.find('[data-testid="notifications-save-error"]').exists()).toBe(true); }); it('Отменить возвращает prefs к оригиналу (auth.user)', async () => { const wrapper = factory(); const checkbox = wrapper.find('[data-testid="pref-new_lead-email"] input'); await checkbox.setValue(true); await wrapper.find('[data-testid="reset-btn"]').trigger('click'); await wrapper.vm.$nextTick(); // dirty снова false → save disabled. expect(wrapper.find('[data-testid="save-btn"]').attributes('disabled')).toBeDefined(); }); it('sound_enabled читается из auth.user (default true)', () => { const wrapper = factory(); const sw = wrapper.find('[data-testid="sound-enabled-switch"] input'); expect((sw.element as HTMLInputElement).checked).toBe(true); }); });