import { describe, it, expect, beforeEach, vi } from 'vitest'; import { mount } from '@vue/test-utils'; import { setActivePinia, createPinia } from 'pinia'; import { AxiosError } from 'axios'; // Частичный мок: сеть (fetchCompetitor/createProjects) — заглушки, но настоящий // autopodborErrorMessage сохраняем, иначе doCreate не сможет замапить код ошибки. vi.mock('../../resources/js/api/autopodbor', async (importOriginal) => { const actual = await importOriginal(); return { ...actual, fetchCompetitor: vi.fn(), createProjects: vi.fn(), }; }); import * as api from '../../resources/js/api/autopodbor'; import { useAutopodborStore } from '../../resources/js/stores/autopodborStore'; import FieldCompetitorScreen from '../../resources/js/views/autopodbor/screens/FieldCompetitorScreen.vue'; const makeNav = (competitorId = 10) => ({ go: vi.fn(), ctx: { competitorId, editProjectId: null, selectedSourceIds: [] }, screen: { value: 'field-competitor' }, }); // Источник БЕЗ проекта в box 'field' — из него создаём проект. const makeSource = (id: number) => ({ id, competitor_id: 10, signal_type: 'site' as const, identifier: `site-${id}.ru`, phone_kind: null, phone_type: null, box: 'field' as const, provenance_url: null, provenance_label: null, created_project_id: null, project: null, }); function axErr(status: number, body: unknown): AxiosError { const e = new AxiosError('err'); // @ts-expect-error — минимальный мок ответа e.response = { status, data: body, statusText: '', headers: {}, config: {} }; return e; } async function mountLoaded() { (api.fetchCompetitor as ReturnType).mockResolvedValue({ competitor: { id: 10, name: 'Окна', is_federal: false, relevance_pct: 90, origin: 'auto', box: 'field', site_url: 'okna.ru', directory_urls: [], studied_at: '2026-01-01', study_run_id: 1, search_run_id: 1, description: '' }, sources: [makeSource(1)], }); const store = useAutopodborStore(); await store.loadCompetitor(10); const wrapper = mount(FieldCompetitorScreen, { global: { provide: { autopodborNav: makeNav() }, stubs: { Transition: false } }, }); await new Promise((r) => setTimeout(r, 50)); return wrapper; } beforeEach(() => { setActivePinia(createPinia()); vi.clearAllMocks(); }); describe('FieldCompetitorScreen — создание проекта из источника (баги 3/5)', () => { it('Баг 5b: doCreate(true) шлёт launch:true; doCreate(false) — launch:false', async () => { (api.createProjects as ReturnType).mockResolvedValue({ data: [{ id: 1, name: 'p' }], launch: null }); const wrapper = await mountLoaded(); // eslint-disable-next-line @typescript-eslint/no-explicit-any const vm = wrapper.vm as any; vm.openCreate(vm.inWork[0]); await wrapper.vm.$nextTick(); await vm.doCreate(false); expect(api.createProjects).toHaveBeenLastCalledWith(expect.objectContaining({ launch: false })); vm.openCreate(vm.inWork[0]); await wrapper.vm.$nextTick(); await vm.doCreate(true); expect(api.createProjects).toHaveBeenLastCalledWith(expect.objectContaining({ launch: true })); }); it('Баг 3: по умолчанию (Вся РФ) шлёт regions: []', async () => { (api.createProjects as ReturnType).mockResolvedValue({ data: [{ id: 1, name: 'p' }], launch: null }); const wrapper = await mountLoaded(); // eslint-disable-next-line @typescript-eslint/no-explicit-any const vm = wrapper.vm as any; vm.openCreate(vm.inWork[0]); await wrapper.vm.$nextTick(); await vm.doCreate(true); expect(api.createProjects).toHaveBeenLastCalledWith(expect.objectContaining({ regions: [] })); }); it('Баг 3: выбранные регионы (Вся РФ снята) шлёт массив кодов', async () => { (api.createProjects as ReturnType).mockResolvedValue({ data: [{ id: 1, name: 'p' }], launch: null }); const wrapper = await mountLoaded(); // eslint-disable-next-line @typescript-eslint/no-explicit-any const vm = wrapper.vm as any; vm.openCreate(vm.inWork[0]); vm.create.vsyaRf = false; vm.create.regions = [24, 16]; await wrapper.vm.$nextTick(); await vm.doCreate(true); expect(api.createProjects).toHaveBeenLastCalledWith(expect.objectContaining({ regions: [24, 16] })); }); it('Баг 3: Вся РФ снята, ни один регион не выбран → не создаём, показываем подсказку', async () => { (api.createProjects as ReturnType).mockResolvedValue({ data: [{ id: 1, name: 'p' }], launch: null }); const wrapper = await mountLoaded(); // eslint-disable-next-line @typescript-eslint/no-explicit-any const vm = wrapper.vm as any; vm.openCreate(vm.inWork[0]); vm.create.vsyaRf = false; vm.create.regions = []; await wrapper.vm.$nextTick(); await vm.doCreate(true); expect(api.createProjects).not.toHaveBeenCalled(); expect(String(vm.toast).toLowerCase()).toContain('регион'); }); it('Баг 5a: ошибка requisites_required → адресный тост про реквизиты, не «баланс»', async () => { (api.createProjects as ReturnType).mockRejectedValue(axErr(422, { error: 'requisites_required' })); const wrapper = await mountLoaded(); // eslint-disable-next-line @typescript-eslint/no-explicit-any const vm = wrapper.vm as any; vm.openCreate(vm.inWork[0]); await wrapper.vm.$nextTick(); await vm.doCreate(true); expect(String(vm.toast).toLowerCase()).toContain('реквизит'); expect(String(vm.toast).toLowerCase()).not.toContain('баланс'); }); it('групповое создание: doCreate шлёт source_ids всех выбранных источников без проекта', async () => { (api.fetchCompetitor as ReturnType).mockResolvedValue({ competitor: { id: 10, name: 'Окна', is_federal: false, relevance_pct: 90, origin: 'auto', box: 'field', site_url: 'okna.ru', directory_urls: [], studied_at: '2026-01-01', study_run_id: 1, search_run_id: 1, description: '' }, sources: [makeSource(1), makeSource(2)], }); (api.createProjects as ReturnType).mockResolvedValue({ data: [], launch: null }); const store = useAutopodborStore(); await store.loadCompetitor(10); const wrapper = mount(FieldCompetitorScreen, { global: { provide: { autopodborNav: makeNav() }, stubs: { Transition: false } }, }); await new Promise((r) => setTimeout(r, 50)); // eslint-disable-next-line @typescript-eslint/no-explicit-any const vm = wrapper.vm as any; vm.selected = [1, 2]; vm.openBulkCreate(); await wrapper.vm.$nextTick(); await vm.doCreate(true); expect(api.createProjects).toHaveBeenLastCalledWith(expect.objectContaining({ source_ids: [1, 2], launch: true })); }); });