Files
portal/app/tests/Frontend/SalesProspectCreateDialog.spec.ts
T
Дмитрий 68428f16c6 @
feat(sales): карточка кандидата — девять полей владельца + правка контактных лиц

Владелец задал список полей карточки и попросил дать менеджеру записывать
контактных лиц прямо в карточке: поиск отдаёт 5-7 ПРЕДПОЛАГАЕМЫХ номеров
директора, живой из них выясняется обзвоном; а если директор перенаправил
к маркетологу — менеджер фиксирует и его, людей может быть 2-3.

- Карточка показывает РОВНО девять полей в его порядке: Юрлицо, Сайт, ИНН фирмы,
  Адрес, Телефон общий, Каналы, Бюджет, Директор, Тел. директора (предполагаемый).
  Убраны Оценка, Достоверность, Реклама, Запросы в Директе, ОГРН, Статус юрлица,
  Личный ИНН директора, Почта директора — данные остаются в payload.
- PATCH /api/sales/prospects/{id}/contacts — список заменяется целиком, пустой
  стирает всех; права как у update() (менеджер свои, начальник любые); главный
  телефон карточки из поиска не трогается.
- ProspectContactsEditor.vue + utils/prospectContacts.ts — один редактор контактов
  на диалог создания и карточку; диалог создания переведён на него.
- Закрывает хвост §16: у 24 живых карточек из поиска контактных лиц не было,
  теперь дозаполняются руками.

Спека §18. Гейты: Pest 223/223 sales, Vitest 196 файлов / 1397 тестов,
vue-tsc чисто, Larastan 0 в своих файлах. TDD RED→GREEN на каждом шаге.

LEFTHOOK_EXCLUDE: larastan/cspell падают на файлах параллельной сессии
tests/Feature/Admin/*Balances*, свои файлы проверены отдельно и чисты.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@
2026-07-18 12:29:04 +03:00

157 lines
5.3 KiB
TypeScript

import { mount, flushPromises } from '@vue/test-utils';
import { createVuetify } from 'vuetify';
import { describe, expect, it, vi, beforeEach } from 'vitest';
vi.mock('../../resources/js/api/sales', () => ({
lookupProspectInn: vi.fn(),
}));
import SalesProspectCreateDialog from '../../resources/js/components/sales/SalesProspectCreateDialog.vue';
import { lookupProspectInn } from '../../resources/js/api/sales';
const vuetify = createVuetify();
interface Contact {
name: string;
position: string;
phones: string[];
}
interface CreateVm {
firmName: string;
legalName: string;
city: string;
site: string;
inn: string;
notes: string;
contacts: Contact[];
error: string;
lookingUp: boolean;
lookup: () => Promise<void>;
submit: () => void;
}
function factory() {
const w = mount(SalesProspectCreateDialog, {
props: { modelValue: true },
global: { plugins: [vuetify] },
});
return { w, vm: w.vm as unknown as CreateVm };
}
describe('SalesProspectCreateDialog', () => {
beforeEach(() => vi.clearAllMocks());
it('без названия не эмитит create', () => {
const { w, vm } = factory();
vm.inn = '5501234560';
vm.submit();
expect(w.emitted('create')).toBeFalsy();
});
it('без ИНН не эмитит create и объясняет почему', () => {
const { w, vm } = factory();
vm.firmName = 'Без ИНН';
vm.submit();
expect(w.emitted('create')).toBeFalsy();
expect(vm.error).toContain('ИНН');
});
it('кривой ИНН не пускаем дальше формы', () => {
const { w, vm } = factory();
vm.firmName = 'Опечатка';
vm.inn = '5501234567';
vm.submit();
expect(w.emitted('create')).toBeFalsy();
});
it('эмитит create с брендом, юрлицом и контактными лицами', () => {
const { w, vm } = factory();
vm.firmName = 'Стоматология «Улыбка»';
vm.legalName = 'ООО «Улыбка Плюс»';
vm.inn = '5501234560';
vm.city = 'Омск';
vm.contacts = [
{ name: 'Иванов Иван', position: 'директор', phones: ['+79130000001', '+73812000002'] },
];
vm.submit();
expect(w.emitted('create')?.[0]?.[0]).toMatchObject({
firm_name: 'Стоматология «Улыбка»',
legal_name: 'ООО «Улыбка Плюс»',
inn: '5501234560',
city: 'Омск',
contacts: [
{ name: 'Иванов Иван', position: 'директор', phones: ['+79130000001', '+73812000002'] },
],
});
});
it('пустые контакты и телефоны не уходят на сервер', () => {
const { w, vm } = factory();
vm.firmName = 'Полупустые';
vm.inn = '5501234560';
vm.contacts = [
{ name: ' ', position: '', phones: ['', ' '] },
{ name: 'Сидоров', position: '', phones: ['+79130000009', ''] },
];
vm.submit();
const payload = w.emitted('create')?.[0]?.[0] as Record<string, unknown>;
expect(payload.contacts).toEqual([{ name: 'Сидоров', position: null, phones: ['+79130000009'] }]);
});
it('поиск по ИНН подставляет юрлицо и город', async () => {
(lookupProspectInn as ReturnType<typeof vi.fn>).mockResolvedValue({
found: true,
legal_name: 'ООО «Улыбка Плюс»',
city: 'Омск',
});
const { vm } = factory();
vm.inn = '5501234560';
await vm.lookup();
await flushPromises();
expect(lookupProspectInn).toHaveBeenCalledWith('5501234560');
expect(vm.legalName).toBe('ООО «Улыбка Плюс»');
expect(vm.city).toBe('Омск');
});
it('поиск не затирает город, если менеджер уже вписал свой', async () => {
(lookupProspectInn as ReturnType<typeof vi.fn>).mockResolvedValue({
found: true,
legal_name: 'ООО «Улыбка Плюс»',
city: 'Омск',
});
const { vm } = factory();
vm.inn = '5501234560';
vm.city = 'Красноярск';
await vm.lookup();
await flushPromises();
expect(vm.city).toBe('Красноярск');
});
it('ДаData не нашла → говорим об этом и не мешаем заполнить руками', async () => {
(lookupProspectInn as ReturnType<typeof vi.fn>).mockResolvedValue({ found: false });
const { vm } = factory();
vm.inn = '5501234560';
await vm.lookup();
await flushPromises();
expect(vm.error).not.toBe('');
expect(vm.legalName).toBe('');
expect(vm.lookingUp).toBe(false);
});
it('ДаData упала → форма остаётся рабочей', async () => {
(lookupProspectInn as ReturnType<typeof vi.fn>).mockRejectedValue(new Error('network'));
const { vm } = factory();
vm.inn = '5501234560';
await vm.lookup();
await flushPromises();
expect(vm.lookingUp).toBe(false);
});
});