Files
portal/app/tests/Frontend/FunnelChart.spec.ts
T
Дмитрий 290e7dbc34 phase2(charts): ActivityChart + FunnelChart - Dashboard закрыт по дизайну
- ActivityChart: native SVG line chart (без chart-library, чтобы не +400KB
  зависимость для статичных дашборд-графиков). Y-grid 5 линий, area-gradient,
  7 точек (предпоследняя выделена primary teal как «сегодня»), 3 tabs
  (Принято/Оплачено/Отказ).
- FunnelChart: segmented bar по 14 статусам + funnel-list (sort desc).
- composables/leadStatuses.ts: snapshot 14 статусов из db/schema.sql:2130
  (НЕ из BRANDBOOK §3.6 - расхождение #1 handoff vs ТЗ из реестра v1.13).
  14 правильных slug'ов: new/viewed/worked/base/missed/negotiations/
  waiting_payment/partnership/paid/closed/test_drive/hot/replacement/final_missed.
- DashboardView интегрирует оба чарта в charts-row (md=7+5).
- cspell-words.txt: ldot, композаблом, инлайнингом, инлайнены.

Vue compiler quirk: withDefaults factory не разрешает референсить module-level
const'ы (checkInvalidScopeReference). Обходим инлайнингом литерала.

Vitest +13 тестов (всего 48/48 за 5.5s):
- ActivityChart 6 (3 tabs + 7 circles + 'сегодня' + custom points + legend)
- FunnelChart 7 (14 segments + 14 list-items + assertion на отсутствие
  'Думает'/'Спам' из handoff + сортировка + colorHex + total)

Stories +2 с 3 variants каждый (Histoire 10/14 за 30.43s).

Регресс: lint+type-check+format OK; vitest 48/48; vite build (DashboardView
chunk 14.9->21.17 KB с чартами); story:build 10/14 за 30.43s; Pest 48/48 за 5.10s.

CLAUDE.md v1.21->v1.22, реестр Открытых_вопросов v1.30->v1.31.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 17:32:58 +03:00

64 lines
2.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, it, expect } from 'vitest';
import { mount } from '@vue/test-utils';
import { createVuetify } from 'vuetify';
import FunnelChart from '../../resources/js/components/charts/FunnelChart.vue';
import { LEAD_STATUSES } from '../../resources/js/composables/leadStatuses';
describe('FunnelChart.vue', () => {
const factory = (props?: Record<string, unknown>) =>
mount(FunnelChart, {
props,
global: { plugins: [createVuetify()] },
});
it('монтируется и содержит заголовок «Воронка»', () => {
const wrapper = factory();
expect(wrapper.text()).toContain('Воронка');
});
it('содержит ровно 14 сегментов в bar (по числу lead_statuses)', () => {
const wrapper = factory();
const segs = wrapper.findAll('.funnel-seg');
expect(segs).toHaveLength(14);
});
it('содержит ровно 14 list-items', () => {
const wrapper = factory();
const items = wrapper.findAll('.funnel-list-item');
expect(items).toHaveLength(14);
});
it('использует правильные slug-имена из schema (НЕ из BRANDBOOK)', () => {
const wrapper = factory();
const text = wrapper.text();
// Проверка что все 14 имён из lead_statuses присутствуют.
LEAD_STATUSES.forEach((s) => {
expect(text).toContain(s.nameRu);
});
// Гарантия что мы НЕ используем имена из BRANDBOOK §3.6 (расхождение #1).
// Например, "Думает" / "Не дозвон." / "Спам" / "КП" — handoff-only.
expect(text).not.toContain('Думает');
expect(text).not.toContain('Спам');
});
it('сортирует список по убыванию count (paid 45 — первый)', () => {
const wrapper = factory();
const names = wrapper.findAll('.funnel-list-item .name').map((n) => n.text());
expect(names[0]).toBe('Оплачено'); // count=45 — самый большой в DEFAULT_COUNTS.
});
it('применяет colorHex из lead_statuses к dots и сегментам', () => {
const wrapper = factory();
const segs = wrapper.findAll<HTMLElement>('.funnel-seg');
// Первый segment в bar — статус с sortOrder=1 (new, цвет #3B82F6).
expect(segs[0].element.style.background).toContain('rgb(59, 130, 246)');
});
it('считает total как сумму counts', () => {
const wrapper = factory({ counts: { new: 10, paid: 20 } });
const text = wrapper.text();
// total = 10 + 20 = 30 (остальные слаги с counts={} → 0).
expect(text).toContain('30 лидов');
});
});