2026-05-09 06:43:21 +03:00
|
|
|
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
2026-05-09 05:33:21 +03:00
|
|
|
|
import { mount, flushPromises } from '@vue/test-utils';
|
|
|
|
|
|
import { createVuetify } from 'vuetify';
|
2026-05-09 06:43:21 +03:00
|
|
|
|
|
|
|
|
|
|
vi.mock('../../resources/js/api/deals', () => ({
|
|
|
|
|
|
createDeal: vi.fn(),
|
2026-05-09 06:58:49 +03:00
|
|
|
|
listProjects: vi.fn(() => Promise.resolve([])),
|
|
|
|
|
|
listManagers: vi.fn(() => Promise.resolve([])),
|
2026-05-09 06:43:21 +03:00
|
|
|
|
}));
|
|
|
|
|
|
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';
|
2026-05-09 05:33:21 +03:00
|
|
|
|
import NewDealDialog from '../../resources/js/components/deals/NewDealDialog.vue';
|
|
|
|
|
|
|
2026-05-09 06:43:21 +03:00
|
|
|
|
const factory = (props: { modelValue: boolean; presetStatus?: string; tenantId?: number } = { modelValue: true }) =>
|
2026-05-09 05:33:21 +03:00
|
|
|
|
mount(NewDealDialog, {
|
|
|
|
|
|
props,
|
|
|
|
|
|
global: {
|
|
|
|
|
|
plugins: [createVuetify()],
|
|
|
|
|
|
stubs: {
|
|
|
|
|
|
VDialog: {
|
|
|
|
|
|
template: '<div class="dialog-stub" v-if="modelValue"><slot /></div>',
|
|
|
|
|
|
props: ['modelValue'],
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-05-09 06:43:21 +03:00
|
|
|
|
beforeEach(() => {
|
|
|
|
|
|
vi.clearAllMocks();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-05-09 05:33:21 +03:00
|
|
|
|
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');
|
|
|
|
|
|
});
|
2026-05-09 06:43:21 +03:00
|
|
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-05-09 06:58:49 +03:00
|
|
|
|
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<string, number>;
|
|
|
|
|
|
};
|
|
|
|
|
|
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,
|
|
|
|
|
|
}),
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-05-09 06:43:21 +03:00
|
|
|
|
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);
|
|
|
|
|
|
});
|
2026-05-09 05:33:21 +03:00
|
|
|
|
});
|