Files
portal/app/tests/Frontend/NewDealDialog.spec.ts
T

326 lines
13 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 { 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: '<div class="dialog-stub" v-if="modelValue"><slot /></div>',
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: 'won' });
await flushPromises();
const vm = wrapper.vm as unknown as { statusSlug: string };
expect(vm.statusSlug).toBe('won');
});
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<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,
}),
);
});
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);
});
it('I3: без tenantId — projectOptions и managerOptions пусты (нет mock-fallback)', async () => {
const wrapper = factory({ modelValue: true }); // нет tenantId
await flushPromises();
const vm = wrapper.vm as unknown as {
projectOptions: string[];
managerOptions: unknown[];
};
expect(vm.projectOptions).toHaveLength(0);
expect(vm.managerOptions).toHaveLength(0);
});
it('C6: при провале loadLookups показывает degradation-alert', async () => {
vi.spyOn(dealsApi, 'listProjects').mockRejectedValue(new Error('network'));
vi.spyOn(dealsApi, 'listManagers').mockRejectedValue(new Error('network'));
const wrapper = factory({ modelValue: true, tenantId: 7 });
await flushPromises();
expect(wrapper.vm.lookupsFailed).toBe(true);
expect(wrapper.find('[data-testid="lookups-error-alert"]').exists()).toBe(true);
});
it('C6: при успешном loadLookups alert отсутствует', async () => {
vi.mocked(dealsApi.listProjects).mockResolvedValue([{ id: 1, name: 'P', tag: null, type: 'manual' }]);
vi.mocked(dealsApi.listManagers).mockResolvedValue([
{ id: 1, email: 'a@b.c', first_name: 'A', last_name: 'B', name: 'A B', initials: 'AB' },
]);
const wrapper = factory({ modelValue: true, tenantId: 7 });
await flushPromises();
expect(wrapper.vm.lookupsFailed).toBe(false);
expect(wrapper.find('[data-testid="lookups-error-alert"]').exists()).toBe(false);
});
});