Files
portal/app/tests/Frontend/SalesProspectDialog.spec.ts
T
Дмитрий 2f6e88e784 feat(sales): замечания владельца по воронке — богатая карточка, сортировка созвонов, счётчики фильтра
Три правки после демо Этапа 1 (все по TDD):
1. Карточка показывает ВСЕ данные поиска из payload (тип ProspectPayload 1:1 с
   dataclass Firm; computed infoRows рендерит юрлицо, директора+личный ИНН,
   контакты, бюджет Директа вилкой, каналы/коллтрекинг, оценку). Демо-сидер
   кладёт полный синтетический payload.
2. Начальнику отдаётся manager_counts; фильтр показывает «Имя (N)».
3. Колонка «Переговоры» сортируется по next_call_at ASC (просроченные сверху).

Гейты: бэк 12/12, фронт 19/19, Larastan 0. НЕ выкачено (ждём разрешения владельца).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 17:52:54 +03:00

125 lines
5.2 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 { mount } from '@vue/test-utils';
import { createVuetify } from 'vuetify';
import { describe, expect, it } from 'vitest';
import SalesProspectDialog from '../../resources/js/components/sales/SalesProspectDialog.vue';
import type { SalesProspect } from '../../resources/js/api/sales';
const vuetify = createVuetify();
function prospect(overrides: Partial<SalesProspect> = {}): SalesProspect {
return {
id: 1,
sales_user_id: 1,
stage: 'new',
firm_name: 'ООО Тест',
city: 'Ростов',
phone: '+7800',
site: 'test.ru',
inn: '6161',
rating_label: 'горячая',
payload: {},
next_call_at: null,
reason: null,
registered_email: null,
notes: null,
...overrides,
};
}
interface DialogVm {
action: string;
reason: string;
nextCallAt: string;
siteHref: string | null;
availableActions: { value: string; title: string }[];
infoRows: { label: string; value: string; href?: string }[];
submit: () => void;
}
function factory(p: SalesProspect) {
return mount(SalesProspectDialog, {
props: { modelValue: true, prospect: p },
global: { plugins: [vuetify] },
});
}
describe('SalesProspectDialog', () => {
it('siteHref добавляет https к домену', () => {
const w = factory(prospect());
expect((w.vm as unknown as DialogVm).siteHref).toBe('https://test.ru');
});
it('для stage=user действие «Отказ» недоступно', () => {
const w = factory(prospect({ stage: 'user' }));
const values = (w.vm as unknown as DialogVm).availableActions.map((a) => a.value);
expect(values).not.toContain('rejected');
});
it('для обычной стадии «Отказ» доступен', () => {
const w = factory(prospect({ stage: 'negotiation' }));
const values = (w.vm as unknown as DialogVm).availableActions.map((a) => a.value);
expect(values).toContain('rejected');
});
it('«Переговоры» без даты — submit не эмитит save', () => {
const w = factory(prospect());
(w.vm as unknown as DialogVm).action = 'negotiation';
(w.vm as unknown as DialogVm).submit();
expect(w.emitted('save')).toBeFalsy();
});
it('«Отказ» с причиной — эмитит save с action=rejected', () => {
const w = factory(prospect({ stage: 'negotiation' }));
(w.vm as unknown as DialogVm).action = 'rejected';
(w.vm as unknown as DialogVm).reason = 'дорого';
(w.vm as unknown as DialogVm).submit();
expect(w.emitted('save')?.[0]?.[0]).toMatchObject({ action: 'rejected', reason: 'дорого' });
});
it('infoRows показывает ВСЕ данные поиска из payload', () => {
const w = factory(
prospect({
inn: '6161000000',
payload: {
legal_name: 'ООО «Тест»',
ogrn: '1234567890123',
address: 'г. Ростов, ул. Ленина 1',
legal_status: 'действующая',
director: 'Иванов Иван Иванович',
director_post: 'директор',
director_is_ip: false,
director_inn: '616100000000',
contact_phone: '+79001234567',
contact_email: 'ivan@test.ru',
direct_budget_min: 80000,
direct_budget: 250000,
direct_keys: 1200,
direct_ads: 30,
channels: ['Яндекс.Директ', 'коллтрекинг'],
},
}),
);
const rows = (w.vm as unknown as DialogVm).infoRows;
const text = rows.map((r) => `${r.label}: ${r.value}`).join(' | ');
expect(text).toContain('Иванов Иван Иванович'); // директор
expect(text).toContain('директор'); // должность
expect(text).toContain('616100000000'); // личный ИНН руководителя (ПДн)
expect(text).toContain('+79001234567'); // тел. директора
expect(text).toContain('ivan@test.ru'); // почта директора
expect(text).toContain('коллтрекинг'); // каналы
expect(text).toContain('1234567890123'); // ОГРН
expect(text).toContain('6161000000'); // ИНН фирмы
expect(text).toContain('г. Ростов, ул. Ленина 1'); // адрес
expect(text).toMatch(/80 000.*250 000/); // вилка бюджета Директа
expect(text).toContain('1 200'); // запросы в Директе (разряды пробелом)
});
it('infoRows пропускает пустые поля payload', () => {
const w = factory(prospect({ payload: {}, inn: null }));
const labels = (w.vm as unknown as DialogVm).infoRows.map((r) => r.label);
expect(labels).not.toContain('ОГРН');
expect(labels).not.toContain('Директор');
});
});