ac3d6975c1
FieldCompetitorScreen: 1) метки ✓ настоящий номер / 🎭 рекламный подменный + плейн-язык «где нашли» + легенда «как читать» (данные в БД и сверка с ЭТАЛОНом НЕ тронуты — перевод только в показе); 2) регион «впечатывается» — свой тайп-ахед в стиле экрана (без Vuetify, чтобы не ломать тесты) в окнах «Создать проект» и «Настройки проекта»; чипы для мультивыбора, подпись округа; 3) галка «Вся РФ (все регионы)» даёт баннер-объявление; 4) групповое создание проектов из всех выбранных источников без проекта (source_ids пачкой), заголовок/текст под «несколько». +тест на групповое создание. Фронт-спеки 24/24 зелёные. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
156 lines
7.4 KiB
TypeScript
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 }));
|
|
});
|
|
});
|