Files
portal/app/tests/Frontend/FieldCompetitorScreenCreate.spec.ts
T
Дмитрий ac3d6975c1 feat(автоподбор/UI): понятные метки источников + впечатываемый регион + групповое создание
FieldCompetitorScreen:
1) метки ✓ настоящий номер / 🎭 рекламный подменный + плейн-язык «где нашли»
   + легенда «как читать» (данные в БД и сверка с ЭТАЛОНом НЕ тронуты — перевод только в показе);
2) регион «впечатывается» — свой тайп-ахед в стиле экрана (без Vuetify, чтобы не ломать тесты)
   в окнах «Создать проект» и «Настройки проекта»; чипы для мультивыбора, подпись округа;
3) галка «Вся РФ (все регионы)» даёт баннер-объявление;
4) групповое создание проектов из всех выбранных источников без проекта (source_ids пачкой),
   заголовок/текст под «несколько». +тест на групповое создание. Фронт-спеки 24/24 зелёные.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 11:12:16 +03:00

156 lines
7.4 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('баланс');
});
it('групповое создание: doCreate шлёт source_ids всех выбранных источников без проекта', async () => {
(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), makeSource(2)],
});
(api.createProjects as ReturnType<typeof vi.fn>).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 }));
});
});