From 84dfbc857ad0b63fe06bda60a8e89cb25fd19c27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Sun, 28 Jun 2026 12:58:19 +0300 Subject: [PATCH] =?UTF-8?q?test(=D1=84=D1=80=D0=BE=D0=BD=D1=82):=20=D0=BF?= =?UTF-8?q?=D1=80=D0=B8=D0=B2=D1=91=D0=BB=20=D1=81=D1=82=D0=B5=D0=BD=D0=B4?= =?UTF-8?q?=20=D0=B2=20=D0=B7=D0=B5=D0=BB=D1=91=D0=BD=D1=8B=D0=B9=20?= =?UTF-8?q?=E2=80=94=2010=20=D0=BF=D1=80=D0=BE=D1=82=D1=83=D1=85=D1=88?= =?UTF-8?q?=D0=B8=D1=85=20=D1=81=D0=BF=D0=B5=D0=BA=D0=BE=D0=B2=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=B4=20=D0=B0=D0=BA=D1=82=D1=83=D0=B0=D0=BB=D1=8C=D0=BD?= =?UTF-8?q?=D1=8B=D0=B5=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE=D0=BD=D0=B5=D0=BD?= =?UTF-8?q?=D1=82=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Все падения — устаревшие ожидания тестов (компоненты менялись намеренно): 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) --- .../Frontend/AdminPricingTiersView.spec.ts | 5 ++-- ...pplierIntegrationView.manual-queue.spec.ts | 24 ++++++++++++++++--- app/tests/Frontend/ChangePasswordCard.spec.ts | 5 ++-- .../Frontend/DealDetailDrawerApi.spec.ts | 4 ++-- app/tests/Frontend/DealsView.spec.ts | 8 +++++-- app/tests/Frontend/ErrorView.spec.ts | 16 ++++++------- app/tests/Frontend/KanbanCard.spec.ts | 9 ++++++- app/tests/Frontend/LegalDocView.spec.ts | 24 +++++++++++-------- app/tests/Frontend/ProjectsView.spec.ts | 13 ++++++---- app/tests/Frontend/SettingsView.spec.ts | 18 ++++++++++---- docs/observer/STATUS.md | 8 +++---- 11 files changed, 89 insertions(+), 45 deletions(-) diff --git a/app/tests/Frontend/AdminPricingTiersView.spec.ts b/app/tests/Frontend/AdminPricingTiersView.spec.ts index 30fa9fa1..183ba5ff 100644 --- a/app/tests/Frontend/AdminPricingTiersView.spec.ts +++ b/app/tests/Frontend/AdminPricingTiersView.spec.ts @@ -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 () => { diff --git a/app/tests/Frontend/AdminSupplierIntegrationView.manual-queue.spec.ts b/app/tests/Frontend/AdminSupplierIntegrationView.manual-queue.spec.ts index d7b6265c..0efec2d6 100644 --- a/app/tests/Frontend/AdminSupplierIntegrationView.manual-queue.spec.ts +++ b/app/tests/Frontend/AdminSupplierIntegrationView.manual-queue.spec.ts @@ -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).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: '
', + 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')); }); diff --git a/app/tests/Frontend/ChangePasswordCard.spec.ts b/app/tests/Frontend/ChangePasswordCard.spec.ts index 8528ef80..f30b608d 100644 --- a/app/tests/Frontend/ChangePasswordCard.spec.ts +++ b/app/tests/Frontend/ChangePasswordCard.spec.ts @@ -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', () => { diff --git a/app/tests/Frontend/DealDetailDrawerApi.spec.ts b/app/tests/Frontend/DealDetailDrawerApi.spec.ts index c1ce64a1..689f5433 100644 --- a/app/tests/Frontend/DealDetailDrawerApi.spec.ts +++ b/app/tests/Frontend/DealDetailDrawerApi.spec.ts @@ -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 () => { diff --git a/app/tests/Frontend/DealsView.spec.ts b/app/tests/Frontend/DealsView.spec.ts index a67613cc..45bf40d6 100644 --- a/app/tests/Frontend/DealsView.spec.ts +++ b/app/tests/Frontend/DealsView.spec.ts @@ -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); }); diff --git a/app/tests/Frontend/ErrorView.spec.ts b/app/tests/Frontend/ErrorView.spec.ts index 18b17059..5cc485a2 100644 --- a/app/tests/Frontend/ErrorView.spec.ts +++ b/app/tests/Frontend/ErrorView.spec.ts @@ -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 () => { diff --git a/app/tests/Frontend/KanbanCard.spec.ts b/app/tests/Frontend/KanbanCard.spec.ts index 5a12896a..bb25b1a5 100644 --- a/app/tests/Frontend/KanbanCard.spec.ts +++ b/app/tests/Frontend/KanbanCard.spec.ts @@ -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('ИП'); diff --git a/app/tests/Frontend/LegalDocView.spec.ts b/app/tests/Frontend/LegalDocView.spec.ts index bc43914f..d5dc68b7 100644 --- a/app/tests/Frontend/LegalDocView.spec.ts +++ b/app/tests/Frontend/LegalDocView.spec.ts @@ -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'); }); }); diff --git a/app/tests/Frontend/ProjectsView.spec.ts b/app/tests/Frontend/ProjectsView.spec.ts index 101e30eb..c44da64c 100644 --- a/app/tests/Frontend/ProjectsView.spec.ts +++ b/app/tests/Frontend/ProjectsView.spec.ts @@ -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'); }); diff --git a/app/tests/Frontend/SettingsView.spec.ts b/app/tests/Frontend/SettingsView.spec.ts index ebca153d..6f806920 100644 --- a/app/tests/Frontend/SettingsView.spec.ts +++ b/app/tests/Frontend/SettingsView.spec.ts @@ -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), ); }); diff --git a/docs/observer/STATUS.md b/docs/observer/STATUS.md index 5e99b459..7a08b8de 100644 --- a/docs/observer/STATUS.md +++ b/docs/observer/STATUS.md @@ -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-сессий.