1aca591558
Перенос ГОТОВОЙ работы соседей: запись c5ca3c438 из ветки
feat/prospects-manual-testing-kp (02.08.2026). Своего такого же не писал — это
задвоило бы правило и устроило столкновение при сведении их ветки.
Зачем. В главной тесты экранов физически ходили в интернет, оставаясь ЗЕЛЁНЫМИ:
ошибка вылетала мимо проверок. Видна была только одна течь — AxiosError на
/api/autopodbor/proposals в общем прогоне; их 93 в 14 файлах.
Сторож делает две вещи, и обе нужны:
1) обрывает запрос — наружу физически ничего не уходит;
2) ЗАПОМИНАЕТ адрес и роняет тест в afterEach — иначе код под тестом глотает
ошибку своим catch, и нарушение снова становится невидимым.
Приёмка ВЫРЕЗАНИЕМ, а не по зелёному: подложен тест, который нарочно лезет в сеть
и глотает ошибку. Сторож покраснел и назвал адрес — то есть вторая половина
работает. Подложка удалена.
Взято не всё: их промт-документ 2026-08-02-PROMT-podgotovka-k-produ-8.md в главную
НЕ переносился. Вспомогательный tests/Frontend/support/fixtures.ts взят — без него
четыре их правленых теста не собираются.
Проверено: 242 файла, 1947 тестов зелёные, 3 пропущены, строки «Errors» больше нет
(была 1). Серверный прогон не затронут.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
111 lines
5.6 KiB
TypeScript
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');
|
|
});
|
|
});
|