Files
portal/app/tests/Frontend/SettingsView.spec.ts
T
Дмитрий c5ca3c4387 fix,тесты интерфейса: запрет на выход в сеть — течь была не в одном тесте, а в 93
Промт называл один тест, который печатал сетевую ошибку. Сторож нашёл 93 теста
в 14 файлах: остальные 92 глушились чужими catch внутри рабочего кода и были
невидимы вообще. Запрета на выход наружу в тестах интерфейса не было ни одного
за всю историю — тот же класс, что закрыли 01.08 на PHP.

Сторож в app/tests/Frontend/setup.ts делает две вещи, и обе нужны: обрывает
запрос и запоминает адрес, роняя тест в afterEach. Без второй половины код,
который глушит отказ своим catch, снова всё прячет.

Там же поимённо заглушены четыре фоновые точки оболочки — счётчик сделок,
состояние автоподбора, статус баланса, отметка о посещении. Их не спрашивает
ни один тест разметки, а отказ у всех четырёх и так проглатывается. Остальные
семь файлов глушат свои вызовы у себя; общий ответ статусов воронки лежит в
tests/Frontend/support/fixtures.ts.

Заглушки повторяют то состояние, которое тесты видели при отказе сети, поэтому
ни одна проверка не переписывалась. track.spec.ts снимает общую заглушку через
vi.unmock — он проверяет сам заглушенный код.

Приёмка: сторож проверен красным трижды — обычный запрос, запрос с проглоченной
ошибкой, fetch. Полный набор интерфейса 233 файла / 1750 тестов / 3 пропущено /
0 падений / 0 ошибок. Линтер интерфейса 0, проверка типов 5 — те же
унаследованные, что были до смены. Рабочий код не тронут.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 17:07:07 +03:00

111 lines
5.6 KiB
TypeScript

import { describe, it, expect, vi } from 'vitest';
// Вкладки настроек на открытии тянут свои данные. Без заглушек тест физически ходил
// в сеть на /api/account/security, /api/api-keys и /api/tenants/me/webhook-settings —
// и всё равно был зелёным: отказы глушились своими catch. Отдаём «пусто», ровно то
// состояние, которое тест видел раньше. Ловит сторож сети в tests/Frontend/setup.ts.
vi.mock('../../resources/js/api/account', async (importOriginal) => ({
...(await importOriginal<typeof import('../../resources/js/api/account')>()),
getAccountSecurity: async () => ({ last_password_change_at: null, sessions: [] }),
}));
vi.mock('../../resources/js/api/apiKeys', async (importOriginal) => ({
...(await importOriginal<typeof import('../../resources/js/api/apiKeys')>()),
listApiKeys: async () => [],
}));
vi.mock('../../resources/js/api/webhooks', async (importOriginal) => ({
...(await importOriginal<typeof import('../../resources/js/api/webhooks')>()),
getWebhookSettings: async () => null,
}));
import { mount } from '@vue/test-utils';
import { createPinia } from 'pinia';
import { createVuetify } from 'vuetify';
import { createRouter, createMemoryHistory } from 'vue-router';
import SettingsView from '../../resources/js/views/SettingsView.vue';
// SettingsView читает route.query.tab (deep-link вкладки) через useRoute(),
// поэтому компоненту нужен router-контекст в тестах.
const router = createRouter({
history: createMemoryHistory(),
routes: [{ path: '/settings', name: 'settings', component: SettingsView }],
});
describe('SettingsView.vue', () => {
const factory = () =>
mount(SettingsView, {
global: { plugins: [createPinia(), createVuetify(), router] },
});
it('монтируется и содержит заголовок «Настройки»', () => {
const wrapper = factory();
expect(wrapper.find('h1').text()).toBe('Настройки');
});
it('содержит ровно 5 nav-tabs (Профиль/Реквизиты/Безопасность/API/Уведомления)', () => {
const wrapper = factory();
const items = wrapper.findAll('.tabs-rail .v-list-item');
expect(items.length).toBe(5);
});
it('содержит все 4 названия рабочих вкладок', () => {
const wrapper = factory();
const text = wrapper.text();
const labels = ['Профиль', 'Безопасность', 'API и Webhook', 'Уведомления'];
labels.forEach((l) => expect(text).toContain(l));
});
it('не содержит placeholder-вкладок и текста «В разработке»', () => {
const wrapper = factory();
const railText = wrapper.find('.tabs-rail').text();
['Команда', 'Интеграции', 'Тихие часы'].forEach((l) => expect(railText).not.toContain(l));
expect(wrapper.text()).not.toContain('В разработке');
});
it('по умолчанию показывает вкладку «Профиль»', () => {
const wrapper = factory();
const text = wrapper.text();
// ProfileTab содержит поля Имя / Фамилия (split из «Полное имя» в audit D1) и Тайм-зона.
expect(text).toContain('Имя');
expect(text).toContain('Фамилия');
expect(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('События × каналы');
// Типы событий из 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');
});
});