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

294 lines
10 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, vi, beforeEach } from 'vitest';
import { mount, flushPromises } from '@vue/test-utils';
import { createVuetify } from 'vuetify';
import { createRouter, createMemoryHistory } from 'vue-router';
import { createPinia, setActivePinia } from 'pinia';
import DealsView from '../../resources/js/views/DealsView.vue';
import KanbanView from '../../resources/js/views/KanbanView.vue';
import { useAuthStore } from '../../resources/js/stores/auth';
import type { ApiDeal } 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,
listDeals: vi.fn(),
listManagers: vi.fn().mockResolvedValue([]),
listProjects: vi.fn().mockResolvedValue([]),
transitionDeals: vi.fn(),
};
});
const dealsApi = await import('../../resources/js/api/deals');
function makeApiDeal(overrides: Partial<ApiDeal> = {}): ApiDeal {
return {
id: 100,
tenant_id: 1,
project_id: 5,
project_name: 'Окна Москва',
phone: '+7 (999) 100-00-01',
contact_name: 'Анна Б.',
status: 'new',
manager_id: 10,
manager_name: 'Иван П.',
manager_initials: 'ИП',
received_at: new Date(Date.now() - 5 * 60 * 1000).toISOString(),
comment: null,
city: null,
project_signal_type: null,
next_reminder_at: null,
...overrides,
};
}
function setupAuth(tenantId: number | null) {
setActivePinia(createPinia());
const auth = useAuthStore();
if (tenantId !== null) {
auth.user = {
id: 1,
email: 'test@example.test',
first_name: 'Test',
last_name: 'User',
tenant_id: tenantId,
totp_enabled: false,
last_login_at: null,
};
}
}
beforeEach(() => {
vi.clearAllMocks();
});
const mountDealsView = async () => {
const router = createRouter({
history: createMemoryHistory(),
routes: [{ path: '/deals', component: DealsView }],
});
await router.push('/deals');
await router.isReady();
return mount(DealsView, {
global: {
plugins: [createVuetify(), router],
stubs: { DealDetailDrawer: true },
},
});
};
const mountKanbanView = () =>
mount(KanbanView, {
global: {
plugins: [createVuetify()],
stubs: { DealDetailDrawer: true, NewDealDialog: true, KanbanColumn: true },
},
});
describe('DealsView ↔ GET /api/deals integration', () => {
it('БЕЗ auth.user.tenant_id — listDeals не вызывается, dealsState пустой', async () => {
setupAuth(null);
const wrapper = await mountDealsView();
await flushPromises();
expect(dealsApi.listDeals).not.toHaveBeenCalled();
const vm = wrapper.vm as unknown as { dealsState: { id: number }[] };
expect(vm.dealsState.length).toBe(0);
});
it('С auth.user.tenant_id — listDeals вызывается с tenantId + replace dealsState', async () => {
setupAuth(1);
vi.mocked(dealsApi.listDeals).mockResolvedValueOnce({
deals: [
makeApiDeal({ id: 200, contact_name: 'Из API #1', status: 'won' }),
makeApiDeal({ id: 201, contact_name: 'Из API #2', status: 'new' }),
],
total: 2,
limit: 20,
offset: 0,
});
const wrapper = await mountDealsView();
await flushPromises();
expect(dealsApi.listDeals).toHaveBeenCalledTimes(1);
expect(dealsApi.listDeals).toHaveBeenCalledWith(expect.objectContaining({ tenantId: 1, limit: 20 }));
const vm = wrapper.vm as unknown as { dealsState: { id: number; name: string }[] };
expect(vm.dealsState).toHaveLength(2);
expect(vm.dealsState.map((d) => d.id).sort()).toEqual([200, 201]);
expect(vm.dealsState.find((d) => d.id === 200)?.name).toBe('Из API #1');
});
it('listDeals reject → fetchError=true, alert виден, dealsState пустой', async () => {
setupAuth(1);
vi.mocked(dealsApi.listDeals).mockRejectedValueOnce(new Error('network'));
const wrapper = await mountDealsView();
await flushPromises();
const vm = wrapper.vm as unknown as { fetchError: boolean; dealsState: unknown[] };
expect(vm.fetchError).toBe(true);
expect(vm.dealsState.length).toBe(0);
// Alert виден.
expect(wrapper.find('[data-testid="fetch-error-alert"]').exists()).toBe(true);
});
it('reload-btn повторно вызывает listDeals', async () => {
setupAuth(1);
vi.mocked(dealsApi.listDeals).mockResolvedValue({
deals: [makeApiDeal({ id: 400 })],
total: 1,
limit: 20,
offset: 0,
});
const wrapper = await mountDealsView();
await flushPromises();
expect(dealsApi.listDeals).toHaveBeenCalledTimes(1);
await wrapper.find('[data-testid="reload-btn"]').trigger('click');
await flushPromises();
expect(dealsApi.listDeals).toHaveBeenCalledTimes(2);
});
it('applyBulkStatus с tenant_id вызывает transitionDeals + локальный optimistic update', async () => {
setupAuth(1);
vi.mocked(dealsApi.listDeals).mockResolvedValueOnce({
deals: [makeApiDeal({ id: 500, status: 'new' }), makeApiDeal({ id: 501, status: 'new' })],
total: 2,
limit: 20,
offset: 0,
});
vi.mocked(dealsApi.transitionDeals).mockResolvedValueOnce({
updated: 2,
requested: 2,
status: 'won',
});
const wrapper = await mountDealsView();
await flushPromises();
const vm = wrapper.vm as unknown as {
selected: number[];
applyBulkStatus: (slug: string) => Promise<void>;
dealsState: { id: number; statusSlug: string }[];
statusToastOpen: boolean;
statusToastText: string;
};
vm.selected = [500, 501];
await flushPromises();
await vm.applyBulkStatus('won');
await flushPromises();
// Optimistic local-update применился до завершения API-вызова.
expect(vm.dealsState.find((d) => d.id === 500)?.statusSlug).toBe('won');
expect(vm.dealsState.find((d) => d.id === 501)?.statusSlug).toBe('won');
expect(dealsApi.transitionDeals).toHaveBeenCalledWith({
tenant_id: 1,
ids: [500, 501],
status: 'won',
});
expect(vm.statusToastOpen).toBe(true);
expect(vm.statusToastText).toContain('Обновлено 2');
});
it('applyBulkStatus с reject → toast с warning, локальный update остаётся (не откатываем)', async () => {
setupAuth(1);
vi.mocked(dealsApi.listDeals).mockResolvedValueOnce({
deals: [makeApiDeal({ id: 600, status: 'new' })],
total: 1,
limit: 20,
offset: 0,
});
vi.mocked(dealsApi.transitionDeals).mockRejectedValueOnce(new Error('500'));
const wrapper = await mountDealsView();
await flushPromises();
const vm = wrapper.vm as unknown as {
selected: number[];
applyBulkStatus: (slug: string) => Promise<void>;
dealsState: { id: number; statusSlug: string }[];
statusToastText: string;
};
vm.selected = [600];
await flushPromises();
await vm.applyBulkStatus('won');
await flushPromises();
expect(vm.dealsState.find((d) => d.id === 600)?.statusSlug).toBe('won');
expect(vm.statusToastText).toContain('Не удалось');
});
});
describe('KanbanView ↔ GET /api/deals integration', () => {
it('БЕЗ tenant_id — listDeals не вызывается', async () => {
setupAuth(null);
mountKanbanView();
await flushPromises();
expect(dealsApi.listDeals).not.toHaveBeenCalled();
});
it('С tenant_id — заполняет колонки по statusSlug + обновляет totalDeals', async () => {
setupAuth(1);
vi.mocked(dealsApi.listDeals).mockResolvedValueOnce({
deals: [
makeApiDeal({ id: 300, status: 'new' }),
makeApiDeal({ id: 301, status: 'won' }),
makeApiDeal({ id: 302, status: 'won' }),
],
total: 3,
limit: 500,
offset: 0,
});
const wrapper = mountKanbanView();
await flushPromises();
const vm = wrapper.vm as unknown as {
dealsByStatus: Record<string, { id: number }[]>;
totalDeals: number;
fetchError: boolean;
};
expect(vm.dealsByStatus.new.map((d) => d.id)).toEqual([300]);
expect(vm.dealsByStatus.won.map((d) => d.id).sort()).toEqual([301, 302]);
expect(vm.totalDeals).toBe(3);
expect(vm.fetchError).toBe(false);
});
it('listDeals reject → fetchError=true, колонки пусты', async () => {
setupAuth(1);
vi.mocked(dealsApi.listDeals).mockRejectedValueOnce(new Error('500'));
const wrapper = mountKanbanView();
await flushPromises();
const vm = wrapper.vm as unknown as {
dealsByStatus: Record<string, unknown[]>;
fetchError: boolean;
};
expect(vm.fetchError).toBe(true);
const filledColumns = Object.values(vm.dealsByStatus).filter((arr) => arr.length > 0);
expect(filledColumns.length).toBe(0);
});
it('reload-btn вызывает listDeals второй раз', async () => {
setupAuth(1);
vi.mocked(dealsApi.listDeals).mockResolvedValue({
deals: [],
total: 0,
limit: 500,
offset: 0,
});
const wrapper = mountKanbanView();
await flushPromises();
expect(dealsApi.listDeals).toHaveBeenCalledTimes(1);
await wrapper.find('[data-testid="reload-btn"]').trigger('click');
await flushPromises();
expect(dealsApi.listDeals).toHaveBeenCalledTimes(2);
});
});