Files
portal/app/tests/Frontend/FieldCompetitorScreenCreate.spec.ts
T
Дмитрий df83fa836c feat(автоподбор): Конкурентное поле — финальный проход склейки, вскрыть всё, группы дублей, телефоны, отсев рекламных номеров 2ГИС
Шаг 1 «Конкурентное поле» — чекпоинт:
- Финальный проход «Найти и объединить дубли» на поле и предложениях; клиент решает по каждой группе, ничего не склеиваем молча.
- Тихая склейка только по сайту и коду справочника; телефон и людный номер у более чем 4 фирм — на решение клиента.
- Колонка phones jsonb; телефон-дубли видны уже на «Предложениях».
- Sonar даёт только имя и тип, сайт всегда через EXA; вскрываем все карточки без гейта «есть сайт».
- Яндекс-карточки через локальный Playwright параллельно; рубрика из заголовка идёт в описание.
- Фикс рекламных номеров: 2ГИС-парсер разбирает телефоны пообъектно и выкидывает рекламные кнопки с platforms/caption чужих фирм.

Тесты: автоподбор 224/224 unit, 105/105 feature; фронт-спеки зелёные. Времянки app/scripts/diag-*/probe-* в гит не входят.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 17:34:03 +03:00

135 lines
6.1 KiB
TypeScript

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<typeof import('../../resources/js/api/autopodbor')>();
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<typeof vi.fn>).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<typeof vi.fn>).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<typeof vi.fn>).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<typeof vi.fn>).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<typeof vi.fn>).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<typeof vi.fn>).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('баланс');
});
});