import { describe, it, expect, beforeEach, vi } from 'vitest';
import { mount, flushPromises } from '@vue/test-utils';
import { createVuetify } from 'vuetify';
vi.mock('../../resources/js/api/deals', () => ({
createDeal: vi.fn(),
listProjects: vi.fn(() => Promise.resolve([])),
listManagers: vi.fn(() => Promise.resolve([])),
}));
vi.mock('../../resources/js/api/client', () => ({
extractErrorMessage: vi.fn((_e, fb?: string) => fb ?? 'err'),
extractValidationErrors: vi.fn(() => null),
apiClient: {},
ensureCsrfCookie: vi.fn(),
}));
import * as dealsApi from '../../resources/js/api/deals';
import NewDealDialog from '../../resources/js/components/deals/NewDealDialog.vue';
const factory = (props: { modelValue: boolean; presetStatus?: string; tenantId?: number } = { modelValue: true }) =>
mount(NewDealDialog, {
props,
global: {
plugins: [createVuetify()],
stubs: {
VDialog: {
template: '
',
props: ['modelValue'],
},
},
},
});
beforeEach(() => {
vi.clearAllMocks();
});
describe('NewDealDialog.vue', () => {
it('не рендерит content при modelValue=false', () => {
const wrapper = factory({ modelValue: false });
expect(wrapper.find('.dialog-stub').exists()).toBe(false);
});
it('рендерит 6 полей: name/phone/project/manager/cost/status + кнопки', () => {
const wrapper = factory();
const text = wrapper.text();
expect(text).toContain('Новая сделка');
['field-name', 'field-phone', 'field-project', 'field-manager', 'field-cost', 'field-status'].forEach((id) => {
expect(wrapper.find(`[data-testid="${id}"]`).exists()).toBe(true);
});
expect(wrapper.find('[data-testid="submit-btn"]').exists()).toBe(true);
expect(wrapper.find('[data-testid="cancel-btn"]').exists()).toBe(true);
});
it('submit без данных не emits created — только validation errors', async () => {
const wrapper = factory();
await wrapper.find('[data-testid="submit-btn"]').trigger('click');
await flushPromises();
expect(wrapper.emitted('created')).toBeUndefined();
// Errors отображаются (хотя бы для name/phone/project/manager)
const text = wrapper.text();
expect(text).toContain('Имя обязательно');
expect(text).toContain('Телефон обязателен');
});
it('submit с валидными данными emits created с правильным deal + закрывает dialog', async () => {
const wrapper = factory();
const vm = wrapper.vm as unknown as {
name: string;
phone: string;
project: string;
manager: { initials: string; name: string };
cost: number;
statusSlug: string;
};
// Заполняем через прямой доступ к ref (компонент использует script setup)
vm.name = 'Тест Тестов';
vm.phone = '+7 (999) 123-45-67';
vm.project = 'Окна Москва';
vm.manager = { initials: 'ИП', name: 'Иван П.' };
vm.cost = 1500;
vm.statusSlug = 'new';
await flushPromises();
await wrapper.find('[data-testid="submit-btn"]').trigger('click');
await flushPromises();
const created = wrapper.emitted('created');
expect(created).toBeDefined();
const deal = (created![0] as unknown[])[0] as {
name: string;
phone: string;
statusSlug: string;
cost: number;
project: string;
};
expect(deal.name).toBe('Тест Тестов');
expect(deal.phone).toBe('+7 (999) 123-45-67');
expect(deal.cost).toBe(1500);
expect(deal.project).toBe('Окна Москва');
expect(deal.statusSlug).toBe('new');
const close = wrapper.emitted('update:modelValue');
expect(close).toBeDefined();
expect(close![0]).toEqual([false]);
});
it('phone < 10 цифр → validation error «Минимум 10 цифр»', async () => {
const wrapper = factory();
const vm = wrapper.vm as unknown as {
name: string;
phone: string;
project: string;
manager: { initials: string; name: string };
};
vm.name = 'X';
vm.phone = '123';
vm.project = 'Окна Москва';
vm.manager = { initials: 'X', name: 'X' };
await flushPromises();
await wrapper.find('[data-testid="submit-btn"]').trigger('click');
await flushPromises();
expect(wrapper.emitted('created')).toBeUndefined();
expect(wrapper.text()).toContain('Минимум 10 цифр');
});
it('presetStatus → statusSlug дефолтит на пресет (для KanbanView)', async () => {
const wrapper = factory({ modelValue: true, presetStatus: 'paid' });
await flushPromises();
const vm = wrapper.vm as unknown as { statusSlug: string };
expect(vm.statusSlug).toBe('paid');
});
it('без tenantId — submit НЕ вызывает API (local-only mode)', async () => {
const wrapper = factory({ modelValue: true }); // tenantId не передан
const vm = wrapper.vm as unknown as {
name: string;
phone: string;
project: string;
manager: { initials: string; name: string };
statusSlug: string;
};
vm.name = 'X';
vm.phone = '+7 (999) 000-00-00';
vm.project = 'Окна Москва';
vm.manager = { initials: 'X', name: 'X' };
vm.statusSlug = 'new';
await flushPromises();
await wrapper.find('[data-testid="submit-btn"]').trigger('click');
await flushPromises();
expect(dealsApi.createDeal).not.toHaveBeenCalled();
expect(wrapper.emitted('created')).toBeDefined();
});
it('с tenantId + успешный backend — emits deal с backend-id', async () => {
vi.mocked(dealsApi.createDeal).mockResolvedValue({
id: 4242,
tenant_id: 1,
project_id: 7,
phone: '+7 (999) 000-00-00',
status: 'new',
contact_name: 'X',
manager_id: null,
received_at: '2026-05-09T12:00:00Z',
});
const wrapper = factory({ modelValue: true, tenantId: 1 });
const vm = wrapper.vm as unknown as {
name: string;
phone: string;
project: string;
manager: { initials: string; name: string };
statusSlug: string;
};
vm.name = 'X';
vm.phone = '+7 (999) 000-00-00';
vm.project = 'Окна Москва';
vm.manager = { initials: 'X', name: 'X' };
vm.statusSlug = 'new';
await flushPromises();
await wrapper.find('[data-testid="submit-btn"]').trigger('click');
await flushPromises();
expect(dealsApi.createDeal).toHaveBeenCalledWith({
tenant_id: 1,
project_name: 'Окна Москва',
phone: '+7 (999) 000-00-00',
contact_name: 'X',
status: 'new',
});
const emitted = wrapper.emitted('created');
expect(emitted).toBeDefined();
const deal = (emitted![0] as unknown[])[0] as { id: number };
expect(deal.id).toBe(4242); // backend-id, не local nextId()
});
it('с tenantId — loadLookups вызывает listManagers + listProjects на open', async () => {
vi.mocked(dealsApi.listProjects).mockResolvedValue([
{ id: 7, name: 'Окна Москва', tag: null, type: 'webhook' },
]);
vi.mocked(dealsApi.listManagers).mockResolvedValue([
{ id: 42, email: 'iv@ex.ru', first_name: 'Иван', last_name: 'Петров', name: 'Иван П.', initials: 'ИП' },
]);
const wrapper = factory({ modelValue: true, tenantId: 1 });
await flushPromises();
expect(dealsApi.listProjects).toHaveBeenCalledWith(1);
expect(dealsApi.listManagers).toHaveBeenCalledWith(1);
// projectOptions / managerOptions заменены backend'ом
const vm = wrapper.vm as unknown as {
projectOptions: string[];
managerOptions: Array<{ name: string; initials: string }>;
managerIdByName: Map;
};
expect(vm.projectOptions).toEqual(['Окна Москва']);
expect(vm.managerOptions).toEqual([{ name: 'Иван П.', initials: 'ИП' }]);
expect(vm.managerIdByName.get('Иван П.')).toBe(42);
});
it('submit с manager → передаёт backend manager_id из mapping', async () => {
vi.mocked(dealsApi.listManagers).mockResolvedValue([
{ id: 99, email: 'x@y.ru', first_name: 'X', last_name: 'Y', name: 'X Y.', initials: 'XY' },
]);
vi.mocked(dealsApi.createDeal).mockResolvedValue({
id: 1,
tenant_id: 1,
project_id: 1,
phone: '+7 (999) 000-00-00',
status: 'new',
contact_name: 'Z',
manager_id: 99,
received_at: '2026-05-09T12:00:00Z',
});
const wrapper = factory({ modelValue: true, tenantId: 1 });
await flushPromises();
const vm = wrapper.vm as unknown as {
name: string;
phone: string;
project: string;
manager: { initials: string; name: string };
statusSlug: string;
};
vm.name = 'Z';
vm.phone = '+7 (999) 000-00-00';
vm.project = 'Окна Москва';
vm.manager = { initials: 'XY', name: 'X Y.' };
vm.statusSlug = 'new';
await flushPromises();
await wrapper.find('[data-testid="submit-btn"]').trigger('click');
await flushPromises();
expect(dealsApi.createDeal).toHaveBeenCalledWith(
expect.objectContaining({
manager_id: 99,
}),
);
});
it('с tenantId + backend-error — fallback на local-id + warning + emit', async () => {
vi.mocked(dealsApi.createDeal).mockRejectedValue(new Error('Network down'));
const wrapper = factory({ modelValue: true, tenantId: 1 });
const vm = wrapper.vm as unknown as {
name: string;
phone: string;
project: string;
manager: { initials: string; name: string };
statusSlug: string;
submitError: string | null;
};
vm.name = 'X';
vm.phone = '+7 (999) 000-00-00';
vm.project = 'Окна Москва';
vm.manager = { initials: 'X', name: 'X' };
vm.statusSlug = 'new';
await flushPromises();
await wrapper.find('[data-testid="submit-btn"]').trigger('click');
await flushPromises();
// Warning показывается, deal эмитится с local-id, диалог остаётся открытым.
expect(wrapper.find('[data-testid="submit-error-alert"]').exists()).toBe(true);
expect(wrapper.emitted('created')).toBeDefined();
// dialog НЕ закрывается на error
const closeEmits = wrapper.emitted('update:modelValue');
expect(closeEmits === undefined || !closeEmits.some((e) => e[0] === false)).toBe(true);
});
});