290e7dbc34
- 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>
64 lines
2.8 KiB
TypeScript
64 lines
2.8 KiB
TypeScript
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 лидов');
|
||
});
|
||
});
|