3bedf10449
UX-request 18.05.2026: - selected.length === 1 → drawer авто-открывается на этой сделке, bulk-полоса скрыта (одну сделку проще менять через drawer) - selected.length >= 2 → drawer закрыт, bulk-полоса видна - selected.length === 0 → как сейчас (drawer по row-click) Vitest 12/12 на DealsView.spec (2 новых теста + 10 существующих, none broken). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
182 lines
9.1 KiB
TypeScript
182 lines
9.1 KiB
TypeScript
import { describe, it, expect, vi, afterEach } 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 * as dealsApi from '../../resources/js/api/deals';
|
|
import { useAuthStore } from '../../resources/js/stores/auth';
|
|
import type { AuthUser } from '../../resources/js/api/auth';
|
|
import type { MockDeal } from '../../resources/js/composables/mockDeals';
|
|
|
|
function apiDeal(id: number, over: Partial<dealsApi.ApiDeal> = {}): dealsApi.ApiDeal {
|
|
return {
|
|
id, tenant_id: 42, project_id: 1, project_name: 'Окна', phone: `+7 916 000-00-0${id}`,
|
|
contact_name: null, status: 'new', manager_id: null, manager_name: null,
|
|
manager_initials: null, received_at: '2026-05-15T09:00:00+00:00',
|
|
comment: null, city: null, project_signal_type: 'call', next_reminder_at: null,
|
|
...over,
|
|
};
|
|
}
|
|
|
|
async function mountDeals(deals: dealsApi.ApiDeal[] = [apiDeal(1), apiDeal(2)], total = 2) {
|
|
setActivePinia(createPinia());
|
|
const auth = useAuthStore();
|
|
auth.user = { id: 1, tenant_id: 42, email: 't@t.com' } as AuthUser;
|
|
const dealsSpy = vi.spyOn(dealsApi, 'listDeals').mockResolvedValue({ deals, total, limit: 20, offset: 0 });
|
|
vi.spyOn(dealsApi, 'listProjects').mockResolvedValue([
|
|
{ id: 1, name: 'Окна', tag: null, type: 'supplier' },
|
|
]);
|
|
const router = createRouter({
|
|
history: createMemoryHistory(),
|
|
routes: [{ path: '/deals', component: DealsView }],
|
|
});
|
|
await router.push('/deals');
|
|
await router.isReady();
|
|
const wrapper = mount(DealsView, {
|
|
global: { plugins: [createVuetify(), router], stubs: { DealDetailDrawer: true, DealsFilters: true } },
|
|
});
|
|
await flushPromises();
|
|
// Reset call history so subsequent vi.spyOn calls in tests start from count=0.
|
|
dealsSpy.mockClear();
|
|
return wrapper;
|
|
}
|
|
|
|
describe('DealsView.vue — реестр лидов', () => {
|
|
it('заголовок «Сделки»', async () => {
|
|
expect((await mountDeals()).find('h1').text()).toBe('Сделки');
|
|
});
|
|
|
|
it('панель экспорта: поля дат + кнопки Excel/CSV', async () => {
|
|
const w = await mountDeals();
|
|
expect(w.find('[data-testid="export-from"]').exists()).toBe(true);
|
|
expect(w.find('[data-testid="export-to"]').exists()).toBe(true);
|
|
expect(w.find('[data-testid="export-xlsx-btn"]').exists()).toBe(true);
|
|
expect(w.find('[data-testid="export-csv-btn"]').exists()).toBe(true);
|
|
});
|
|
|
|
it('селектор «Показывать по» с вариантами 10/20/50', async () => {
|
|
const w = await mountDeals();
|
|
const toggle = w.find('[data-testid="perpage-toggle"]');
|
|
expect(toggle.exists()).toBe(true);
|
|
['10', '20', '50'].forEach((n) => expect(toggle.text()).toContain(n));
|
|
});
|
|
|
|
it('НЕТ кнопки «Новая сделка» и режима «Корзина»', async () => {
|
|
const w = await mountDeals();
|
|
// «Новая сделка» присутствует как статус-пилюля в таблице (slug `new` —
|
|
// редизайн воронки 2026-05-17), поэтому проверяем отсутствие именно
|
|
// КНОПКИ создания сделки: ручное создание убрано в реестре лидов.
|
|
const buttons = w.findAll('button');
|
|
expect(buttons.some((b) => b.text().includes('Новая сделка'))).toBe(false);
|
|
expect(w.text()).not.toContain('Корзина');
|
|
});
|
|
|
|
it('загружает сделки в dealsState через API', async () => {
|
|
const w = await mountDeals([apiDeal(1), apiDeal(2), apiDeal(3)], 3);
|
|
const vm = w.vm as unknown as { dealsState: MockDeal[]; total: number };
|
|
expect(vm.dealsState.length).toBe(3);
|
|
expect(vm.total).toBe(3);
|
|
});
|
|
|
|
it('openPanel выбирает сделку, повторный клик закрывает', async () => {
|
|
const w = await mountDeals();
|
|
const vm = w.vm as unknown as {
|
|
dealsState: MockDeal[]; panelOpen: boolean; selectedDeal: MockDeal | null;
|
|
openPanel: (d: MockDeal) => void;
|
|
};
|
|
vm.openPanel(vm.dealsState[0]);
|
|
expect(vm.panelOpen).toBe(true);
|
|
expect(vm.selectedDeal?.id).toBe(1);
|
|
vm.openPanel(vm.dealsState[0]);
|
|
expect(vm.panelOpen).toBe(false);
|
|
});
|
|
|
|
it('при selected=1 drawer авто-открывается, bulk-полоса скрыта (18.05.2026 ux)', async () => {
|
|
const w = await mountDeals();
|
|
const vm = w.vm as unknown as {
|
|
selected: number[]; panelOpen: boolean; selectedDeal: MockDeal | null;
|
|
};
|
|
vm.selected = [1];
|
|
await flushPromises();
|
|
expect(vm.panelOpen).toBe(true);
|
|
expect(vm.selectedDeal?.id).toBe(1);
|
|
expect(w.find('[data-testid="bulk-bar"]').exists()).toBe(false);
|
|
});
|
|
|
|
it('при selected≥2 drawer закрывается, bulk-полоса видна (18.05.2026 ux)', async () => {
|
|
const w = await mountDeals();
|
|
const vm = w.vm as unknown as {
|
|
selected: number[]; panelOpen: boolean; dealsState: MockDeal[];
|
|
openPanel: (d: MockDeal) => void;
|
|
};
|
|
vm.openPanel(vm.dealsState[0]);
|
|
expect(vm.panelOpen).toBe(true);
|
|
vm.selected = [1, 2];
|
|
await flushPromises();
|
|
expect(vm.panelOpen).toBe(false);
|
|
expect(w.find('[data-testid="bulk-bar"]').exists()).toBe(true);
|
|
});
|
|
|
|
it('bulk-bar появляется при выборе и applyBulkStatus меняет статус', async () => {
|
|
const w = await mountDeals();
|
|
const vm = w.vm as unknown as {
|
|
selected: number[]; dealsState: MockDeal[]; applyBulkStatus: (s: string) => Promise<void>;
|
|
};
|
|
vi.spyOn(dealsApi, 'transitionDeals').mockResolvedValue({ updated: 2, requested: 2, status: 'viewed' });
|
|
vm.selected = [1, 2];
|
|
await flushPromises();
|
|
expect(w.find('[data-testid="bulk-bar"]').exists()).toBe(true);
|
|
await vm.applyBulkStatus('viewed');
|
|
expect(vm.dealsState.find((d) => d.id === 1)?.statusSlug).toBe('viewed');
|
|
});
|
|
|
|
it('exportByRange xlsx вызывает exportDealsByRange', async () => {
|
|
const w = await mountDeals();
|
|
const spy = vi.spyOn(dealsApi, 'exportDealsByRange').mockResolvedValue(new Blob());
|
|
Object.defineProperty(URL, 'createObjectURL', { value: vi.fn(() => 'blob:m'), configurable: true });
|
|
Object.defineProperty(URL, 'revokeObjectURL', { value: vi.fn(), configurable: true });
|
|
vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => {});
|
|
const vm = w.vm as unknown as { exportByRange: (f: string) => Promise<void>; exportToastOpen: boolean };
|
|
await vm.exportByRange('xlsx');
|
|
expect(spy).toHaveBeenCalledOnce();
|
|
expect(vm.exportToastOpen).toBe(true);
|
|
});
|
|
|
|
it('смена фильтра вызывает loadDeals ровно один раз (без двойного fetch)', async () => {
|
|
const w = await mountDeals();
|
|
const vm = w.vm as unknown as { filterStatus: string | null; page: number };
|
|
// Установим spy до смены page, чтобы перехватить все вызовы
|
|
const spy = vi.spyOn(dealsApi, 'listDeals').mockResolvedValue({ deals: [], total: 0, limit: 20, offset: 0 });
|
|
// Переходим на страницу 3 — это вызовет watch(page) → loadDeals один раз
|
|
vm.page = 3;
|
|
await flushPromises();
|
|
spy.mockClear(); // сбрасываем счётчик: интересует только смена фильтра
|
|
// Смена фильтра при page=3: A10 fix должен лишь сбросить page→1 (без прямого loadDeals),
|
|
// затем watch(page) делает ровно один fetch
|
|
vm.filterStatus = 'viewed';
|
|
await flushPromises();
|
|
expect(spy).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('loadDeals reject → dealsState пустой + fetchError', async () => {
|
|
setActivePinia(createPinia());
|
|
const auth = useAuthStore();
|
|
auth.user = { id: 1, tenant_id: 42, email: 't@t.com' } as AuthUser;
|
|
vi.spyOn(dealsApi, 'listDeals').mockRejectedValue(new Error('500'));
|
|
vi.spyOn(dealsApi, 'listProjects').mockResolvedValue([]);
|
|
const router = createRouter({ history: createMemoryHistory(), routes: [{ path: '/deals', component: DealsView }] });
|
|
await router.push('/deals');
|
|
await router.isReady();
|
|
const w = mount(DealsView, {
|
|
global: { plugins: [createVuetify(), router], stubs: { DealDetailDrawer: true, DealsFilters: true } },
|
|
});
|
|
await flushPromises();
|
|
const vm = w.vm as unknown as { dealsState: MockDeal[]; fetchError: boolean };
|
|
expect(vm.dealsState.length).toBe(0);
|
|
expect(vm.fetchError).toBe(true);
|
|
});
|
|
});
|
|
|
|
afterEach(() => vi.restoreAllMocks());
|