f55b91cfa4
Закрывает архитектурное расхождение v1.28 — Tab сохранял prefs только локально без API. Backend events не совпадали с handoff'ом. Backend: - PATCH /api/auth/me/notification-preferences под auth:sanctum. - Replace-семантика: незадекларированные events/channels отбрасываются. - userResource расширен: notification_preferences + sound_enabled. - UserFactory с schema-default JSON (Eloquent не перечитывает после INSERT, DB-DEFAULT JSONB виден как null без явного override). - Pest +10: 401 / replace / неизвестные events/channels отбрасываются / 422 без prefs / sound_enabled опционален / bool-cast 1/'1' / replace- семантика (отсутствующие events исчезают). Frontend: - api/auth.ts: типы NotificationChannel/EventKey/Preferences + updateNotificationPreferences helper. AuthUser получил optional поля. - NotificationsTab.vue переписан под schema: 8 событий (new_lead/reminder/low_balance/zero_balance/topup_success/ invoice_paid/new_device_login/marketing) × 3 канала (inapp/push/email, НЕ sms). Sync-init prefs (без onMounted — иначе v-if блокирует рендер и тесты mount-then-find падают). dirty через computed-сравнение с originalPrefs snapshot. save async + success/error alerts. - SettingsView.spec.ts: legacy event-имена → schema-aligned. - Vitest +10: 8 schema events / 3 channels (НЕ sms) / legacy отсутствуют / читает prefs из user / save calls API + alerts / Отменить возвращает. cspell-words: +prefs. PHPStan baseline регенерирован. Pest 315/315 (+10) за 36.73 сек, 1130 assertions. Vitest 349/349 (+10) за 20.42 сек. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
96 lines
4.1 KiB
TypeScript
96 lines
4.1 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { mount } from '@vue/test-utils';
|
|
import { createPinia } from 'pinia';
|
|
import { createVuetify } from 'vuetify';
|
|
import SettingsView from '../../resources/js/views/SettingsView.vue';
|
|
|
|
describe('SettingsView.vue', () => {
|
|
const factory = () =>
|
|
mount(SettingsView, {
|
|
global: { plugins: [createPinia(), createVuetify()] },
|
|
});
|
|
|
|
it('монтируется и содержит заголовок «Настройки»', () => {
|
|
const wrapper = factory();
|
|
expect(wrapper.find('h1').text()).toBe('Настройки');
|
|
});
|
|
|
|
it('содержит ровно 8 nav-tabs', () => {
|
|
const wrapper = factory();
|
|
const items = wrapper.findAll('.tabs-rail .v-list-item');
|
|
expect(items.length).toBe(8);
|
|
});
|
|
|
|
it('содержит все 8 названий вкладок', () => {
|
|
const wrapper = factory();
|
|
const text = wrapper.text();
|
|
const labels = [
|
|
'Профиль',
|
|
'Безопасность',
|
|
'Проекты',
|
|
'Команда',
|
|
'API и Webhook',
|
|
'Интеграции',
|
|
'Тихие часы',
|
|
'Уведомления',
|
|
];
|
|
labels.forEach((l) => expect(text).toContain(l));
|
|
});
|
|
|
|
it('по умолчанию показывает вкладку «Профиль»', () => {
|
|
const wrapper = factory();
|
|
const text = wrapper.text();
|
|
// ProfileTab содержит «Полное имя» и поле email.
|
|
expect(text).toContain('Полное имя');
|
|
expect(text).toContain('Тайм-зона');
|
|
});
|
|
|
|
it('placeholder-вкладки показывают «В разработке»', async () => {
|
|
const wrapper = factory();
|
|
// Кликаем по «Проекты» — placeholder-вкладка.
|
|
const items = wrapper.findAll('.tabs-rail .v-list-item');
|
|
const projectsItem = items.find((i) => i.text().includes('Проекты'));
|
|
expect(projectsItem).toBeDefined();
|
|
await projectsItem!.trigger('click');
|
|
await wrapper.vm.$nextTick();
|
|
expect(wrapper.text()).toContain('В разработке');
|
|
});
|
|
|
|
it('переключение на «Уведомления» показывает матрицу 8×3', async () => {
|
|
const wrapper = factory();
|
|
const items = wrapper.findAll('.tabs-rail .v-list-item');
|
|
const notifItem = items.find((i) => i.text().includes('Уведомления'));
|
|
await notifItem!.trigger('click');
|
|
await wrapper.vm.$nextTick();
|
|
const text = wrapper.text();
|
|
expect(text).toContain('События × каналы');
|
|
// 8 типов событий из schema users.notification_preferences.
|
|
['Новый лид', 'Напоминание', 'Низкий баланс', 'Нулевой баланс', 'Анонсы и промо'].forEach((e) =>
|
|
expect(text).toContain(e),
|
|
);
|
|
});
|
|
|
|
it('переключение на «Безопасность» показывает 2FA и сессии', async () => {
|
|
const wrapper = factory();
|
|
const items = wrapper.findAll('.tabs-rail .v-list-item');
|
|
const secItem = items.find((i) => i.text().includes('Безопасность'));
|
|
await secItem!.trigger('click');
|
|
await wrapper.vm.$nextTick();
|
|
const text = wrapper.text();
|
|
expect(text).toContain('Двухфакторная авторизация');
|
|
expect(text).toContain('Активные сессии');
|
|
});
|
|
|
|
it('переключение на «API и Webhook» показывает API-ключ и signing secret', async () => {
|
|
const wrapper = factory();
|
|
const items = wrapper.findAll('.tabs-rail .v-list-item');
|
|
const apiItem = items.find((i) => i.text().includes('API'));
|
|
await apiItem!.trigger('click');
|
|
await wrapper.vm.$nextTick();
|
|
const text = wrapper.text();
|
|
expect(text).toContain('API-ключ');
|
|
expect(text).toContain('Signing secret');
|
|
expect(text).toContain('HMAC');
|
|
});
|
|
});
|