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

215 lines
7.9 KiB
TypeScript
Raw Normal View History

import { describe, it, expect, vi, beforeEach } from 'vitest';
import { mount, flushPromises } from '@vue/test-utils';
import { createVuetify } from 'vuetify';
import { createPinia, setActivePinia } from 'pinia';
import DealDetailDrawer from '../../resources/js/components/deals/DealDetailDrawer.vue';
import { MOCK_DEALS } from '../../resources/js/composables/mockDeals';
import { MOCK_EVENTS } from '../../resources/js/composables/mockDealEvents';
import type { GetDealResponse, ApiDealEvent } from '../../resources/js/api/deals';
vi.mock('../../resources/js/api/deals', async (importOriginal) => {
const orig = await importOriginal<typeof import('../../resources/js/api/deals')>();
return {
...orig,
getDeal: vi.fn(),
updateDeal: vi.fn(),
};
});
const dealsApi = await import('../../resources/js/api/deals');
beforeEach(() => {
vi.clearAllMocks();
setActivePinia(createPinia());
});
const factory = (props: { open: boolean; tenantId?: number }) =>
mount(DealDetailDrawer, {
props: { deal: MOCK_DEALS[0], ...props },
global: {
plugins: [createVuetify()],
stubs: {
VNavigationDrawer: {
template: '<div class="drawer-stub" v-if="modelValue"><slot /></div>',
props: ['modelValue'],
},
},
},
});
function makeApiEvent(overrides: Partial<ApiDealEvent> = {}): ApiDealEvent {
return {
id: 100,
event: 'deal.created',
context: { source: 'webhook' },
created_at: new Date(Date.now() - 30 * 60 * 1000).toISOString(),
actor: null,
...overrides,
};
}
describe('DealDetailDrawer ↔ GET /api/deals/{id} integration', () => {
it('БЕЗ tenantId — getDeal не вызывается, показываются MOCK_EVENTS', async () => {
const wrapper = factory({ open: true });
await flushPromises();
expect(dealsApi.getDeal).not.toHaveBeenCalled();
const items = wrapper.findAll('.timeline-item');
expect(items).toHaveLength(MOCK_EVENTS.length);
});
it('С tenantId — getDeal вызывается, events заменяются на API', async () => {
const apiResponse: GetDealResponse = {
deal: {
id: MOCK_DEALS[0].id,
tenant_id: 1,
project_id: 5,
project_name: 'Окна Москва',
phone: '+7 (999) 100-00-01',
contact_name: 'Anna',
comment: null,
status: 'new',
manager_id: null,
manager_name: null,
manager_initials: null,
received_at: new Date().toISOString(),
assigned_at: null,
},
events: [
makeApiEvent({ id: 1, event: 'deal.created', context: { source: 'webhook' } }),
makeApiEvent({
id: 2,
event: 'deal.status_changed',
context: { from: 'new', to: 'paid' },
actor: { id: 1, name: 'Иван П.', initials: 'ИП' },
}),
],
};
vi.mocked(dealsApi.getDeal).mockResolvedValueOnce(apiResponse);
const wrapper = factory({ open: true, tenantId: 1 });
await flushPromises();
expect(dealsApi.getDeal).toHaveBeenCalledWith(MOCK_DEALS[0].id, 1);
const items = wrapper.findAll('.timeline-item');
expect(items).toHaveLength(2);
// status_changed event имеет detail "new → paid".
expect(wrapper.text()).toContain('new → paid');
});
it('getDeal reject → eventsFetchError=true, alert виден, MOCK_EVENTS как fallback', async () => {
vi.mocked(dealsApi.getDeal).mockRejectedValueOnce(new Error('500'));
const wrapper = factory({ open: true, tenantId: 1 });
await flushPromises();
const vm = wrapper.vm as unknown as { eventsFetchError: boolean };
expect(vm.eventsFetchError).toBe(true);
expect(wrapper.find('[data-testid="events-fetch-error-alert"]').exists()).toBe(true);
// Fallback на MOCK_EVENTS.
const items = wrapper.findAll('.timeline-item');
expect(items).toHaveLength(MOCK_EVENTS.length);
});
it('open=false → getDeal не вызывается', async () => {
factory({ open: false, tenantId: 1 });
await flushPromises();
expect(dealsApi.getDeal).not.toHaveBeenCalled();
});
it('saveComment вызывает updateDeal + toast success + reload events', async () => {
const apiResponse: GetDealResponse = {
deal: {
id: MOCK_DEALS[0].id,
tenant_id: 1,
project_id: 5,
project_name: 'Окна',
phone: '+7 (999) 100-00-01',
contact_name: 'Anna',
comment: 'old',
status: 'new',
manager_id: null,
manager_name: null,
manager_initials: null,
received_at: new Date().toISOString(),
assigned_at: null,
},
events: [],
};
vi.mocked(dealsApi.getDeal).mockResolvedValue(apiResponse);
vi.mocked(dealsApi.updateDeal).mockResolvedValueOnce({
...apiResponse.deal,
comment: 'новая заметка',
});
const wrapper = factory({ open: true, tenantId: 1 });
await flushPromises();
const vm = wrapper.vm as unknown as {
commentDraft: string;
saveComment: () => Promise<void>;
commentToastOpen: boolean;
commentToastText: string;
commentSaveError: boolean;
};
expect(vm.commentDraft).toBe('old'); // загружено из getDeal
vm.commentDraft = 'новая заметка';
await vm.saveComment();
await flushPromises();
expect(dealsApi.updateDeal).toHaveBeenCalledWith(MOCK_DEALS[0].id, {
tenant_id: 1,
comment: 'новая заметка',
});
expect(vm.commentToastOpen).toBe(true);
expect(vm.commentToastText).toContain('сохранён');
expect(vm.commentSaveError).toBe(false);
// reload events после save — getDeal вызвался ещё раз.
expect(dealsApi.getDeal).toHaveBeenCalledTimes(2);
});
it('saveComment reject → toast warning + commentSaveError=true', async () => {
vi.mocked(dealsApi.getDeal).mockResolvedValueOnce({
deal: {
id: MOCK_DEALS[0].id,
tenant_id: 1,
project_id: 5,
project_name: null,
phone: '+7 (999)',
contact_name: null,
comment: null,
status: 'new',
manager_id: null,
manager_name: null,
manager_initials: null,
received_at: null,
assigned_at: null,
},
events: [],
});
vi.mocked(dealsApi.updateDeal).mockRejectedValueOnce(new Error('500'));
const wrapper = factory({ open: true, tenantId: 1 });
await flushPromises();
const vm = wrapper.vm as unknown as {
commentDraft: string;
saveComment: () => Promise<void>;
commentSaveError: boolean;
commentToastText: string;
};
vm.commentDraft = 'новый';
await vm.saveComment();
await flushPromises();
expect(vm.commentSaveError).toBe(true);
expect(vm.commentToastText).toContain('Не удалось');
});
it('comment-section не показывается без tenantId (read-only mode)', async () => {
const wrapper = factory({ open: true });
await flushPromises();
expect(wrapper.find('[data-testid="comment-section"]').exists()).toBe(false);
});
});