test(фронт): привёл стенд в зелёный — 10 протухших спеков под актуальные компоненты
Accessibility (Pa11y live) / a11y (push) Has been cancelled
Accessibility (Pa11y live) / a11y (push) Has been cancelled
Все падения — устаревшие ожидания тестов (компоненты менялись намеренно): SettingsView (роутер+вкладка Реквизиты+события), LegalDoc (реальные доки под ЮKassa), ProjectsView (BulkActionsBar v-show→isVisible), ErrorView (убран фейк REQ/INC), PricingTiers (формат «500 ₽»), KanbanCard (costKopecks→«—»), ChangePassword (дата из API), DealDetail (русские ярлыки статусов), DealsView (RuDateField на v-menu), SupplierIntegration (window.confirm→v-dialog). Изменены ТОЛЬКО тесты, компоненты не тронуты. Полный прогон: 127 файлов / 992 теста зелёные. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -51,8 +51,9 @@ describe('AdminPricingTiersView', () => {
|
||||
it('renders 7 tier rows from /api/admin/pricing-tiers', async () => {
|
||||
const wrapper = mount(AdminPricingTiersView, { global: { plugins: [vuetify] } });
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
expect(wrapper.text()).toContain('500.00');
|
||||
expect(wrapper.text()).toContain('250.00');
|
||||
// fmtRub форматирует как «500 ₽» (целое, без хвостовых нулей), не «500.00».
|
||||
expect(wrapper.text()).toContain('500 ₽');
|
||||
expect(wrapper.text()).toContain('250 ₽');
|
||||
});
|
||||
|
||||
it('shows "все свыше" for tier 7 with leads_in_tier=null', async () => {
|
||||
|
||||
@@ -44,18 +44,36 @@ describe('AdminSupplierIntegrationView — manual queue section', () => {
|
||||
expect(text).toContain('B1');
|
||||
});
|
||||
|
||||
it('clicking «Отметить выполнено» calls resolve endpoint', async () => {
|
||||
it('clicking «Отметить выполнено» → подтверждение в диалоге → calls resolve endpoint', async () => {
|
||||
(axios.post as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
data: { resolved: true, external_id: 700123 },
|
||||
});
|
||||
vi.spyOn(window, 'confirm').mockReturnValue(true);
|
||||
|
||||
const wrapper = mount(AdminSupplierIntegrationView, { global: { plugins: [vuetify] } });
|
||||
// window.confirm заменён на v-dialog (UI-аудит). Стабим VDialog passthrough,
|
||||
// чтобы контент диалога рендерился инлайн и кнопка «Подтверждаю» была кликабельна.
|
||||
const wrapper = mount(AdminSupplierIntegrationView, {
|
||||
global: {
|
||||
plugins: [vuetify],
|
||||
stubs: {
|
||||
VDialog: {
|
||||
template: '<div class="dialog-stub" v-if="modelValue"><slot /></div>',
|
||||
props: ['modelValue'],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
|
||||
// Клик по «Отметить выполнено» открывает диалог подтверждения (askResolve).
|
||||
const btn = wrapper.find('[data-testid="resolve-1"]');
|
||||
expect(btn.exists()).toBe(true);
|
||||
await btn.trigger('click');
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
// Подтверждаем в диалоге → doResolve → POST.
|
||||
const confirmBtn = wrapper.findAll('button').find((b) => b.text().includes('Подтверждаю'));
|
||||
expect(confirmBtn).toBeTruthy();
|
||||
await confirmBtn!.trigger('click');
|
||||
|
||||
expect(axios.post).toHaveBeenCalledWith(expect.stringContaining('/manual-queue/1/resolve'));
|
||||
});
|
||||
|
||||
@@ -15,9 +15,10 @@ describe('ChangePasswordCard (Q.DEFER.003 sub-B)', () => {
|
||||
});
|
||||
|
||||
it('shows last-change hint text', () => {
|
||||
// Дата берётся из GET /api/account/security; без backend (в тесте) — честное
|
||||
// «не менялся» (хардкод-демо «12.04.2026» убран намеренно, не показываем фейк).
|
||||
const wrapper = factory();
|
||||
expect(wrapper.text()).toContain('Последняя смена: 12.04.2026');
|
||||
expect(wrapper.text()).toContain('26 дней назад');
|
||||
expect(wrapper.text()).toContain('Последняя смена: не менялся');
|
||||
});
|
||||
|
||||
it('renders «Сменить пароль» button with lock-reset icon', () => {
|
||||
|
||||
@@ -88,8 +88,8 @@ describe('DealDetailBody ↔ GET /api/deals/{id} integration', () => {
|
||||
expect(dealsApi.getDeal).toHaveBeenCalledWith(MOCK_DEALS[0].id, 1);
|
||||
const items = wrapper.findAll('.timeline-item');
|
||||
expect(items).toHaveLength(2);
|
||||
// status_changed event имеет detail "new → won".
|
||||
expect(wrapper.text()).toContain('new → won');
|
||||
// status_changed мапит слаги в русские ярлыки воронки: new→«Новая сделка», won→«Сделка».
|
||||
expect(wrapper.text()).toContain('Новая сделка → Сделка');
|
||||
});
|
||||
|
||||
it('getDeal reject → eventsFetchError=true, alert виден, events пуст (I3)', async () => {
|
||||
|
||||
@@ -57,8 +57,12 @@ describe('DealsView.vue — реестр лидов', () => {
|
||||
|
||||
it('панель экспорта: поля дат + кнопки Excel/CSV', async () => {
|
||||
const w = await mountDeals();
|
||||
expect(w.find('[data-testid="export-from"]').exists()).toBe(true);
|
||||
expect(w.find('[data-testid="export-to"]').exists()).toBe(true);
|
||||
const panel = w.find('.export-panel');
|
||||
expect(panel.exists()).toBe(true);
|
||||
// 2 поля даты: RuDateField построен на v-menu, поэтому data-testid не доходит
|
||||
// до DOM-элемента — проверяем по input'ам активаторов (v-text-field) внутри панели.
|
||||
expect(panel.findAll('input').length).toBeGreaterThanOrEqual(2);
|
||||
// Кнопки — v-btn, data-testid доходит до корня button.
|
||||
expect(w.find('[data-testid="export-xlsx-btn"]').exists()).toBe(true);
|
||||
expect(w.find('[data-testid="export-csv-btn"]').exists()).toBe(true);
|
||||
});
|
||||
|
||||
@@ -38,25 +38,23 @@ describe('ErrorView.vue', () => {
|
||||
expect(text).toContain('Все рабочие экраны Лидерра доступны через дашборд');
|
||||
});
|
||||
|
||||
it('errorCode=403 показывает «403 / У вас нет доступа» + RequestId', async () => {
|
||||
it('errorCode=403 показывает «403 / У вас нет доступа» (фейк-RequestId убран)', async () => {
|
||||
const wrapper = await mountErrorView('403');
|
||||
const text = wrapper.text();
|
||||
expect(wrapper.find('.err-code').text()).toBe('403');
|
||||
expect(text).toContain('У вас нет доступа');
|
||||
expect(text).toContain('REQ-3F8A2-0007');
|
||||
expect(text).toContain('Запрос');
|
||||
// Хардкод «REQ-3F8A2-0007» убран намеренно (не показываем фейк как настоящее).
|
||||
expect(text).not.toContain('REQ-3F8A2-0007');
|
||||
});
|
||||
|
||||
it('errorCode=500 показывает «500 / Что-то пошло не так» + IncidentId + status-list', async () => {
|
||||
it('errorCode=500 показывает «500 / Что-то пошло не так» (фейк-Incident/status-list убраны)', async () => {
|
||||
const wrapper = await mountErrorView('500');
|
||||
const text = wrapper.text();
|
||||
expect(wrapper.find('.err-code').text()).toBe('500');
|
||||
expect(text).toContain('Что-то пошло не так');
|
||||
expect(text).toContain('INC-2026-0507-0034');
|
||||
expect(text).toContain('Инцидент');
|
||||
// status-list только на 500.
|
||||
expect(text).toContain('API · OK');
|
||||
expect(text).toContain('Telegram · деградация');
|
||||
// Хардкод «INC-2026-0507-0034» + фейк-список статусов убраны намеренно.
|
||||
expect(text).not.toContain('INC-2026-0507-0034');
|
||||
expect(text).not.toContain('Telegram · деградация');
|
||||
});
|
||||
|
||||
it('404 содержит «На дашборд» primary + «Назад» secondary', async () => {
|
||||
|
||||
@@ -12,7 +12,9 @@ describe('KanbanCard.vue', () => {
|
||||
});
|
||||
|
||||
it('рендерит имя, телефон, проект и стоимость', () => {
|
||||
const wrapper = factory(MOCK_DEALS[0]);
|
||||
// Карточка показывает ФАКТ списания (costKopecks), а не legacy cost; при отсутствии
|
||||
// списания — «—». Формат проверяем на сделке с costKopecks=185000 → «1 850 ₽».
|
||||
const wrapper = factory({ ...MOCK_DEALS[0], costKopecks: 185000 });
|
||||
const text = wrapper.text();
|
||||
expect(text).toContain('Анна Соколова');
|
||||
expect(text).toContain('+7 (916) 871-23-45');
|
||||
@@ -20,6 +22,11 @@ describe('KanbanCard.vue', () => {
|
||||
expect(text).toMatch(/1\s+850\s*₽/);
|
||||
});
|
||||
|
||||
it('показывает «—» в стоимости когда списания ещё не было (costKopecks=null)', () => {
|
||||
const wrapper = factory({ ...MOCK_DEALS[0], costKopecks: null });
|
||||
expect(wrapper.find('.card-cost').text()).toBe('—');
|
||||
});
|
||||
|
||||
it('показывает initials менеджера', () => {
|
||||
const wrapper = factory(MOCK_DEALS[0]);
|
||||
expect(wrapper.text()).toContain('ИП');
|
||||
|
||||
@@ -24,9 +24,9 @@ const mountAt = async (path: string) => {
|
||||
};
|
||||
|
||||
describe('LegalDocView.vue', () => {
|
||||
it('рендерит «Договор-оферта» на /legal/offer', async () => {
|
||||
it('рендерит «Публичная оферта» на /legal/offer', async () => {
|
||||
const wrapper = await mountAt('/legal/offer');
|
||||
expect(wrapper.text()).toContain('Договор-оферта');
|
||||
expect(wrapper.text()).toContain('Публичная оферта');
|
||||
});
|
||||
|
||||
it('рендерит «Политика конфиденциальности» на /legal/privacy', async () => {
|
||||
@@ -34,17 +34,21 @@ describe('LegalDocView.vue', () => {
|
||||
expect(wrapper.text()).toContain('Политика конфиденциальности');
|
||||
});
|
||||
|
||||
it('показывает честную заглушку «документ готовится», а не фейк-текст', async () => {
|
||||
it('показывает реальный текст оферты (рабочая редакция под ЮKassa), а не заглушку', async () => {
|
||||
const wrapper = await mountAt('/legal/offer');
|
||||
const notice = wrapper.find('[data-testid="legal-stub-notice"]');
|
||||
expect(notice.exists()).toBe(true);
|
||||
expect(notice.text()).toContain('готовится');
|
||||
const text = wrapper.text();
|
||||
// Реальные разделы + дата редакции из content/legalDocs.ts.
|
||||
expect(text).toContain('Предмет');
|
||||
expect(text).toContain('Реквизиты Исполнителя');
|
||||
expect(text).toContain('Редакция от 2026-06-24');
|
||||
expect(text).not.toContain('готовится');
|
||||
});
|
||||
|
||||
it('содержит ссылку возврата ко входу', async () => {
|
||||
it('политика конфиденциальности содержит реальные разделы (оператор/права субъекта)', async () => {
|
||||
const wrapper = await mountAt('/legal/privacy');
|
||||
const back = wrapper.find('a.legal-back');
|
||||
expect(back.exists()).toBe(true);
|
||||
expect(back.attributes('href')).toBe('/login');
|
||||
const text = wrapper.text();
|
||||
expect(text).toContain('Оператор');
|
||||
expect(text).toContain('Права субъекта');
|
||||
expect(text).toContain('Редакция от 2026-06-24');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -104,7 +104,7 @@ describe('ProjectsView', () => {
|
||||
cards[0].vm.$emit('toggle-select', 1);
|
||||
cards[1].vm.$emit('toggle-select', 2);
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(wrapper.findComponent({ name: 'BulkActionsBar' }).exists()).toBe(true);
|
||||
expect(wrapper.findComponent({ name: 'BulkActionsBar' }).isVisible()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -155,7 +155,8 @@ describe('ProjectsView × ProjectDetailsDrawer integration', () => {
|
||||
expect(drawer.exists()).toBe(true);
|
||||
expect(drawer.classes()).not.toContain('open');
|
||||
// BulkActionsBar uses v-if; should not exist.
|
||||
expect(wrapper.findComponent({ name: 'BulkActionsBar' }).exists()).toBe(false);
|
||||
// BulkActionsBar теперь v-show (всегда смонтирован, скрыт display:none) → проверяем видимость.
|
||||
expect(wrapper.findComponent({ name: 'BulkActionsBar' }).isVisible()).toBe(false);
|
||||
// .has-drawer class should not be on the view root.
|
||||
expect(wrapper.find('.projects-view').classes()).not.toContain('has-drawer');
|
||||
});
|
||||
@@ -173,7 +174,8 @@ describe('ProjectsView × ProjectDetailsDrawer integration', () => {
|
||||
expect(drawer.exists()).toBe(true);
|
||||
expect(drawer.classes()).toContain('open');
|
||||
// BulkActionsBar should NOT exist (size < 2).
|
||||
expect(wrapper.findComponent({ name: 'BulkActionsBar' }).exists()).toBe(false);
|
||||
// BulkActionsBar теперь v-show (всегда смонтирован, скрыт display:none) → проверяем видимость.
|
||||
expect(wrapper.findComponent({ name: 'BulkActionsBar' }).isVisible()).toBe(false);
|
||||
// .has-drawer class should be present.
|
||||
expect(wrapper.find('.projects-view').classes()).toContain('has-drawer');
|
||||
});
|
||||
@@ -192,7 +194,7 @@ describe('ProjectsView × ProjectDetailsDrawer integration', () => {
|
||||
expect(drawer.exists()).toBe(true);
|
||||
expect(drawer.classes()).not.toContain('open');
|
||||
// BulkActionsBar should exist (size >= 2).
|
||||
expect(wrapper.findComponent({ name: 'BulkActionsBar' }).exists()).toBe(true);
|
||||
expect(wrapper.findComponent({ name: 'BulkActionsBar' }).isVisible()).toBe(true);
|
||||
// .has-drawer class should NOT be present.
|
||||
expect(wrapper.find('.projects-view').classes()).not.toContain('has-drawer');
|
||||
});
|
||||
@@ -215,7 +217,8 @@ describe('ProjectsView × ProjectDetailsDrawer integration', () => {
|
||||
// Both should be hidden now.
|
||||
const drawerAfter = wrapper.find('aside.project-details-drawer');
|
||||
expect(drawerAfter.classes()).not.toContain('open');
|
||||
expect(wrapper.findComponent({ name: 'BulkActionsBar' }).exists()).toBe(false);
|
||||
// BulkActionsBar теперь v-show (всегда смонтирован, скрыт display:none) → проверяем видимость.
|
||||
expect(wrapper.findComponent({ name: 'BulkActionsBar' }).isVisible()).toBe(false);
|
||||
expect(wrapper.find('.projects-view').classes()).not.toContain('has-drawer');
|
||||
});
|
||||
|
||||
|
||||
@@ -2,12 +2,20 @@ import { describe, it, expect } from 'vitest';
|
||||
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()] },
|
||||
global: { plugins: [createPinia(), createVuetify(), router] },
|
||||
});
|
||||
|
||||
it('монтируется и содержит заголовок «Настройки»', () => {
|
||||
@@ -15,10 +23,10 @@ describe('SettingsView.vue', () => {
|
||||
expect(wrapper.find('h1').text()).toBe('Настройки');
|
||||
});
|
||||
|
||||
it('содержит ровно 4 nav-tabs (placeholder-вкладки убраны, audit D6/D7)', () => {
|
||||
it('содержит ровно 5 nav-tabs (Профиль/Реквизиты/Безопасность/API/Уведомления)', () => {
|
||||
const wrapper = factory();
|
||||
const items = wrapper.findAll('.tabs-rail .v-list-item');
|
||||
expect(items.length).toBe(4);
|
||||
expect(items.length).toBe(5);
|
||||
});
|
||||
|
||||
it('содержит все 4 названия рабочих вкладок', () => {
|
||||
@@ -52,8 +60,8 @@ describe('SettingsView.vue', () => {
|
||||
await wrapper.vm.$nextTick();
|
||||
const text = wrapper.text();
|
||||
expect(text).toContain('События × каналы');
|
||||
// 8 типов событий из schema users.notification_preferences.
|
||||
['Новый лид', 'Напоминание', 'Низкий баланс', 'Нулевой баланс', 'Анонсы и промо'].forEach((e) =>
|
||||
// Типы событий из schema users.notification_preferences (актуальный набор).
|
||||
['Новый лид', 'Низкий баланс', 'Нулевой баланс', 'Пополнение успешно', 'Анонсы и промо'].forEach((e) =>
|
||||
expect(text).toContain(e),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Brain Status (auto-generated)
|
||||
|
||||
Last updated: 2026-06-28T08:09:57.221Z
|
||||
Last updated: 2026-06-28T09:05:52.697Z
|
||||
|
||||
| Контролёр | Состояние | Детали |
|
||||
|---|---|---|
|
||||
@@ -125,9 +125,9 @@ Episodes since last run: 542 / threshold: 10
|
||||
|
||||
| PID | Имя | CPU-время | Возраст |
|
||||
|---|---|---|---|
|
||||
| 3440 | MsMpEng | 17.61ч | NaNч |
|
||||
| 21928 | Code | 7.87ч | 0.0ч |
|
||||
| 1212 | svchost | 4.49ч | NaNч |
|
||||
| 3440 | MsMpEng | 17.84ч | NaNч |
|
||||
| 21928 | Code | 8.07ч | 0.0ч |
|
||||
| 1212 | svchost | 4.54ч | 0.0ч |
|
||||
|
||||
⚠️ Проверь, не «осиротевшие» ли это процессы от завершённых Claude-сессий.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user