Files
portal/app/tests/Frontend/DealsListIntegration.spec.ts
T
Дмитрий 830a652588 phase2(trash-bin): GET /api/deals?only_deleted + «Корзина» в DealsView
Расширяет stages 5/6 (soft-delete + 8-сек undo) до постоянного доступа
к удалённым сделкам через отдельный view-mode.

Backend (DealController::index):
- Новый query-param only_deleted=true.
- withTrashed() + whereNotNull('deleted_at') — обход global scope
  SoftDeletes + явный фильтр для NO-OP idempotency.
- Все остальные фильтры применимы и в trash-mode.

Pest +3 (DealIndexTest):
- only_deleted=true → только soft-deleted (alive скрыты).
- Без only_deleted → soft-deleted скрыты (default behavior).
- RLS+app-фильтр изолирует чужие удалённые.

Frontend:
- ListDealsParams.onlyDeleted?: boolean + axios mapping.
- DealsView: trashMode ref + toggleTrashMode (clear selected + reload) +
  applyBulkRestoreFromTrash (optimistic remove + bulkRestoreDeals + toast).
- UI changes в trash-mode:
  - Заголовок «Сделки» → «Корзина».
  - Toggle-btn 'mdi-arrow-left К сделкам' (warning-flat) вместо
    'mdi-trash-can-outline Корзина' (outlined).
  - Скрыты Экспорт + Новая сделка.
  - Скрыт chiprow filter-bar.
  - Info-alert «Корзина: показаны удалённые сделки».
  - Bulk-bar: только Восстановить (mdi-restore success-tonal) + clear;
    status/export/delete скрыты.

Vitest +2 (DealsListIntegration):
- toggleTrashMode → trashMode=true + listDeals с onlyDeleted=true.
- applyBulkRestoreFromTrash → bulkRestoreDeals + remove from state +
  toast «Восстановлено 2».

PHPStan baseline регенерирован.

Регресс:
- Lint+type-check+format passed.
- Vitest 321/321 за 19.60 сек (+2 от 319).
- Vite build 1.04 сек.
- Pint + PHPStan passed.
- Pest 269/269 за 29.12 сек (+3 от 266, 1009 assertions).

Реестр v1.72→v1.73 / CLAUDE.md v1.63→v1.64.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 10:27:11 +03:00

689 lines
25 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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(),
exportDeals: vi.fn(),
exportDealsXlsx: vi.fn(),
bulkDeleteDeals: vi.fn(),
bulkRestoreDeals: 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(),
...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, NewDealDialog: 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 не вызывается, fallback на MOCK_DEALS', async () => {
setupAuth(null);
const wrapper = await mountDealsView();
await flushPromises();
expect(dealsApi.listDeals).not.toHaveBeenCalled();
// MOCK_DEALS содержит 12 элементов — fallback виден.
const vm = wrapper.vm as unknown as { dealsState: { id: number }[] };
expect(vm.dealsState.length).toBeGreaterThan(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: 'paid' }),
makeApiDeal({ id: 201, contact_name: 'Из API #2', status: 'new' }),
],
total: 2,
limit: 200,
offset: 0,
});
const wrapper = await mountDealsView();
await flushPromises();
expect(dealsApi.listDeals).toHaveBeenCalledTimes(1);
expect(dealsApi.listDeals).toHaveBeenCalledWith(expect.objectContaining({ tenantId: 1, limit: 200 }));
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 виден, MOCK_DEALS остаётся как fallback', 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).toBeGreaterThan(0);
// Alert виден.
expect(wrapper.find('[data-testid="fetch-error-alert"]').exists()).toBe(true);
});
it('toggleTrashMode переключает trashMode + listDeals вызывается с onlyDeleted=true', async () => {
setupAuth(1);
// Начальный fetch (нормальный режим)
vi.mocked(dealsApi.listDeals).mockResolvedValueOnce({
deals: [makeApiDeal({ id: 600 })],
total: 1,
limit: 200,
offset: 0,
});
// После toggle в trash
vi.mocked(dealsApi.listDeals).mockResolvedValueOnce({
deals: [makeApiDeal({ id: 700, contact_name: 'Удалённый' })],
total: 1,
limit: 200,
offset: 0,
});
const wrapper = await mountDealsView();
await flushPromises();
expect(dealsApi.listDeals).toHaveBeenCalledTimes(1);
expect(dealsApi.listDeals).toHaveBeenLastCalledWith(
expect.objectContaining({ tenantId: 1, onlyDeleted: false }),
);
const vm = wrapper.vm as unknown as {
trashMode: boolean;
toggleTrashMode: () => void;
dealsState: { id: number }[];
};
vm.toggleTrashMode();
await flushPromises();
expect(vm.trashMode).toBe(true);
expect(dealsApi.listDeals).toHaveBeenCalledTimes(2);
expect(dealsApi.listDeals).toHaveBeenLastCalledWith(
expect.objectContaining({ tenantId: 1, onlyDeleted: true }),
);
expect(vm.dealsState.find((d) => d.id === 700)).toBeDefined();
});
it('applyBulkRestoreFromTrash восстанавливает + убирает из dealsState', async () => {
setupAuth(1);
vi.mocked(dealsApi.listDeals).mockResolvedValueOnce({
deals: [makeApiDeal({ id: 800 }), makeApiDeal({ id: 801 })],
total: 2,
limit: 200,
offset: 0,
});
vi.mocked(dealsApi.bulkRestoreDeals).mockResolvedValueOnce({
restored: 2,
requested: 2,
});
const wrapper = await mountDealsView();
await flushPromises();
const vm = wrapper.vm as unknown as {
selected: number[];
applyBulkRestoreFromTrash: () => Promise<void>;
dealsState: { id: number }[];
deleteToastText: string;
};
vm.selected = [800, 801];
await flushPromises();
await vm.applyBulkRestoreFromTrash();
await flushPromises();
expect(dealsApi.bulkRestoreDeals).toHaveBeenCalledWith({ tenant_id: 1, ids: [800, 801] });
// Восстановленные убраны из текущего trash-списка.
expect(vm.dealsState.find((d) => d.id === 800)).toBeUndefined();
expect(vm.dealsState.find((d) => d.id === 801)).toBeUndefined();
expect(vm.deleteToastText).toContain('Восстановлено 2');
});
it('reload-btn повторно вызывает listDeals', async () => {
setupAuth(1);
vi.mocked(dealsApi.listDeals).mockResolvedValue({
deals: [makeApiDeal({ id: 400 })],
total: 1,
limit: 200,
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: 200,
offset: 0,
});
vi.mocked(dealsApi.transitionDeals).mockResolvedValueOnce({
updated: 2,
requested: 2,
status: 'paid',
});
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('paid');
await flushPromises();
// Optimistic local-update применился до завершения API-вызова.
expect(vm.dealsState.find((d) => d.id === 500)?.statusSlug).toBe('paid');
expect(vm.dealsState.find((d) => d.id === 501)?.statusSlug).toBe('paid');
expect(dealsApi.transitionDeals).toHaveBeenCalledWith({
tenant_id: 1,
ids: [500, 501],
status: 'paid',
});
expect(vm.statusToastOpen).toBe(true);
expect(vm.statusToastText).toContain('Обновлено 2');
});
it('applyBulkStatus БЕЗ tenant_id — только локальный update, transitionDeals НЕ вызывается', async () => {
setupAuth(null);
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 }[];
};
vm.selected = [1];
await flushPromises();
await vm.applyBulkStatus('paid');
await flushPromises();
expect(dealsApi.transitionDeals).not.toHaveBeenCalled();
expect(vm.dealsState.find((d) => d.id === 1)?.statusSlug).toBe('paid');
});
it('applyBulkExport(xlsx) с tenant_id вызывает exportDealsXlsx и триггерит download', async () => {
setupAuth(1);
vi.mocked(dealsApi.listDeals).mockResolvedValueOnce({
deals: [makeApiDeal({ id: 700 })],
total: 1,
limit: 200,
offset: 0,
});
const fakeBlob = new Blob(['fake xlsx'], { type: 'application/octet-stream' });
vi.mocked(dealsApi.exportDealsXlsx).mockResolvedValueOnce(fakeBlob);
const createUrlSpy = vi.fn(() => 'blob:xlsx');
const revokeSpy = vi.fn();
Object.defineProperty(URL, 'createObjectURL', { value: createUrlSpy, configurable: true });
Object.defineProperty(URL, 'revokeObjectURL', { value: revokeSpy, configurable: true });
const clickSpy = vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => {});
const wrapper = await mountDealsView();
await flushPromises();
const vm = wrapper.vm as unknown as {
selected: number[];
applyBulkExport: (format?: string) => Promise<void>;
exportToastText: string;
};
vm.selected = [700];
await flushPromises();
await vm.applyBulkExport(); // default = xlsx
await flushPromises();
expect(dealsApi.exportDealsXlsx).toHaveBeenCalledWith({
tenant_id: 1,
ids: [700],
});
expect(dealsApi.exportDeals).not.toHaveBeenCalled();
expect(createUrlSpy).toHaveBeenCalledTimes(1);
expect(clickSpy).toHaveBeenCalledTimes(1);
expect(vm.exportToastText).toContain('XLSX');
clickSpy.mockRestore();
});
it('applyBulkExport(csv) с tenant_id вызывает exportDeals (CSV branch)', async () => {
setupAuth(1);
vi.mocked(dealsApi.listDeals).mockResolvedValueOnce({
deals: [makeApiDeal({ id: 701 })],
total: 1,
limit: 200,
offset: 0,
});
vi.mocked(dealsApi.exportDeals).mockResolvedValueOnce('id;...');
Object.defineProperty(URL, 'createObjectURL', { value: vi.fn(() => 'blob:'), configurable: true });
const clickSpy = vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => {});
const wrapper = await mountDealsView();
await flushPromises();
const vm = wrapper.vm as unknown as {
selected: number[];
applyBulkExport: (format?: string) => Promise<void>;
exportToastText: string;
};
vm.selected = [701];
await flushPromises();
await vm.applyBulkExport('csv');
await flushPromises();
expect(dealsApi.exportDeals).toHaveBeenCalledTimes(1);
expect(dealsApi.exportDealsXlsx).not.toHaveBeenCalled();
expect(vm.exportToastText).toContain('CSV');
clickSpy.mockRestore();
});
it('applyBulkExport(xlsx) reject → fallback на local CSV', async () => {
setupAuth(1);
vi.mocked(dealsApi.listDeals).mockResolvedValueOnce({
deals: [makeApiDeal({ id: 702 })],
total: 1,
limit: 200,
offset: 0,
});
vi.mocked(dealsApi.exportDealsXlsx).mockRejectedValueOnce(new Error('500'));
Object.defineProperty(URL, 'createObjectURL', { value: vi.fn(() => 'blob:'), configurable: true });
const clickSpy = vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => {});
const wrapper = await mountDealsView();
await flushPromises();
const vm = wrapper.vm as unknown as {
selected: number[];
applyBulkExport: () => Promise<void>;
exportToastText: string;
};
vm.selected = [702];
await flushPromises();
await vm.applyBulkExport();
await flushPromises();
expect(vm.exportToastText).toContain('Backend недоступен');
// local CSV всё равно стриггерил download
expect(clickSpy).toHaveBeenCalled();
clickSpy.mockRestore();
});
it('applyBulkDelete с tenant_id вызывает bulkDeleteDeals + optimistic local-removal', async () => {
setupAuth(1);
vi.mocked(dealsApi.listDeals).mockResolvedValueOnce({
deals: [makeApiDeal({ id: 800, status: 'new' }), makeApiDeal({ id: 801, status: 'new' })],
total: 2,
limit: 200,
offset: 0,
});
vi.mocked(dealsApi.bulkDeleteDeals).mockResolvedValueOnce({
deleted: 2,
requested: 2,
});
const wrapper = await mountDealsView();
await flushPromises();
const vm = wrapper.vm as unknown as {
selected: number[];
applyBulkDelete: () => Promise<void>;
dealsState: { id: number }[];
deleteToastOpen: boolean;
deleteToastText: string;
};
vm.selected = [800, 801];
await flushPromises();
await vm.applyBulkDelete();
await flushPromises();
// Optimistic — обе сделки убраны из state.
expect(vm.dealsState.find((d) => d.id === 800)).toBeUndefined();
expect(vm.dealsState.find((d) => d.id === 801)).toBeUndefined();
expect(dealsApi.bulkDeleteDeals).toHaveBeenCalledWith({
tenant_id: 1,
ids: [800, 801],
});
expect(vm.deleteToastOpen).toBe(true);
expect(vm.deleteToastText).toContain('Удалено 2');
});
it('applyBulkDelete без tenant_id — только локально, bulkDeleteDeals НЕ вызывается', async () => {
setupAuth(null);
const wrapper = await mountDealsView();
await flushPromises();
const vm = wrapper.vm as unknown as {
selected: number[];
applyBulkDelete: () => Promise<void>;
dealsState: { id: number }[];
};
const before = vm.dealsState.length;
vm.selected = [1, 2];
await flushPromises();
await vm.applyBulkDelete();
await flushPromises();
expect(dealsApi.bulkDeleteDeals).not.toHaveBeenCalled();
expect(vm.dealsState.length).toBe(before - 2);
});
it('applyBulkDelete reject → warning toast, локальный update остаётся (не откатываем)', async () => {
setupAuth(1);
vi.mocked(dealsApi.listDeals).mockResolvedValueOnce({
deals: [makeApiDeal({ id: 900 })],
total: 1,
limit: 200,
offset: 0,
});
vi.mocked(dealsApi.bulkDeleteDeals).mockRejectedValueOnce(new Error('500'));
const wrapper = await mountDealsView();
await flushPromises();
const vm = wrapper.vm as unknown as {
selected: number[];
applyBulkDelete: () => Promise<void>;
dealsState: { id: number }[];
deleteToastText: string;
};
vm.selected = [900];
await flushPromises();
await vm.applyBulkDelete();
await flushPromises();
expect(vm.dealsState.find((d) => d.id === 900)).toBeUndefined(); // optimistic
expect(vm.deleteToastText).toContain('Не удалось');
});
it('bulk-delete + undo восстанавливает сделки + вызывает bulkRestoreDeals', async () => {
setupAuth(1);
vi.mocked(dealsApi.listDeals).mockResolvedValueOnce({
deals: [makeApiDeal({ id: 1000, contact_name: 'A' }), makeApiDeal({ id: 1001, contact_name: 'B' })],
total: 2,
limit: 200,
offset: 0,
});
vi.mocked(dealsApi.bulkDeleteDeals).mockResolvedValueOnce({
deleted: 2,
requested: 2,
});
vi.mocked(dealsApi.bulkRestoreDeals).mockResolvedValueOnce({
restored: 2,
requested: 2,
});
const wrapper = await mountDealsView();
await flushPromises();
const vm = wrapper.vm as unknown as {
selected: number[];
applyBulkDelete: () => Promise<void>;
undoBulkDelete: () => Promise<void>;
dealsState: { id: number }[];
lastDeletedSnapshot: { id: number }[];
deleteToastText: string;
};
// Удаляем
vm.selected = [1000, 1001];
await flushPromises();
await vm.applyBulkDelete();
await flushPromises();
expect(vm.dealsState.find((d) => d.id === 1000)).toBeUndefined();
expect(vm.lastDeletedSnapshot).toHaveLength(2);
// Undo
await vm.undoBulkDelete();
await flushPromises();
expect(dealsApi.bulkRestoreDeals).toHaveBeenCalledWith({ tenant_id: 1, ids: [1000, 1001] });
expect(vm.dealsState.find((d) => d.id === 1000)).toBeDefined();
expect(vm.dealsState.find((d) => d.id === 1001)).toBeDefined();
expect(vm.lastDeletedSnapshot).toHaveLength(0); // cleared after undo
expect(vm.deleteToastText).toContain('Восстановлено 2');
});
it('undoBulkDelete без tenant_id — только локально, bulkRestoreDeals НЕ вызывается', async () => {
setupAuth(null);
const wrapper = await mountDealsView();
await flushPromises();
const vm = wrapper.vm as unknown as {
selected: number[];
applyBulkDelete: () => Promise<void>;
undoBulkDelete: () => Promise<void>;
dealsState: { id: number }[];
lastDeletedSnapshot: { id: number }[];
};
const sample = vm.dealsState[0];
vm.selected = [sample.id];
await flushPromises();
await vm.applyBulkDelete();
await flushPromises();
expect(vm.dealsState.find((d) => d.id === sample.id)).toBeUndefined();
await vm.undoBulkDelete();
await flushPromises();
expect(dealsApi.bulkRestoreDeals).not.toHaveBeenCalled();
expect(vm.dealsState.find((d) => d.id === sample.id)).toBeDefined();
});
it('undoBulkDelete reject → warning toast, локальное восстановление остаётся', async () => {
setupAuth(1);
vi.mocked(dealsApi.listDeals).mockResolvedValueOnce({
deals: [makeApiDeal({ id: 1100 })],
total: 1,
limit: 200,
offset: 0,
});
vi.mocked(dealsApi.bulkDeleteDeals).mockResolvedValueOnce({ deleted: 1, requested: 1 });
vi.mocked(dealsApi.bulkRestoreDeals).mockRejectedValueOnce(new Error('500'));
const wrapper = await mountDealsView();
await flushPromises();
const vm = wrapper.vm as unknown as {
selected: number[];
applyBulkDelete: () => Promise<void>;
undoBulkDelete: () => Promise<void>;
dealsState: { id: number }[];
deleteToastText: string;
};
vm.selected = [1100];
await flushPromises();
await vm.applyBulkDelete();
await flushPromises();
await vm.undoBulkDelete();
await flushPromises();
expect(vm.dealsState.find((d) => d.id === 1100)).toBeDefined(); // optimistic
expect(vm.deleteToastText).toContain('Не удалось восстановить');
});
it('applyBulkStatus с reject → toast с warning, локальный update остаётся (не откатываем)', async () => {
setupAuth(1);
vi.mocked(dealsApi.listDeals).mockResolvedValueOnce({
deals: [makeApiDeal({ id: 600, status: 'new' })],
total: 1,
limit: 200,
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('paid');
await flushPromises();
expect(vm.dealsState.find((d) => d.id === 600)?.statusSlug).toBe('paid');
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: 'paid' }),
makeApiDeal({ id: 302, status: 'paid' }),
],
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.paid.map((d) => d.id).sort()).toEqual([301, 302]);
expect(vm.totalDeals).toBe(3);
expect(vm.fetchError).toBe(false);
});
it('listDeals reject → fetchError=true, MOCK_DEALS остаётся в колонках', 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);
// Хотя бы одна колонка с mock-сделками заполнена (изначальный state).
const filledColumns = Object.values(vm.dealsByStatus).filter((arr) => arr.length > 0);
expect(filledColumns.length).toBeGreaterThan(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);
});
});