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

161 lines
6.6 KiB
TypeScript
Raw Normal View History

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