4387333118
Фича «Конкурентное поле» на dev до уровня прототипа 2026-06-29-konkurentnoe-pole-proto.html.
Данные: box (proposal|field) на competitors+sources; phone_type city/mobile/tollfree рядом
с phone_kind (вариант C). 3 миграции, дефолты тарифов 300/50.
API (AutopodborController): GET /field (+счётчики), GET /proposals, PATCH/DELETE competitors
и sources с гвардами активного проекта, переключение box, POST /competitors/manual (+directory_urls),
competitor(id) обогащён box+project-статусом; projectStatus отдаёт limit/delivered/days/regions.
Смена источника проекта = PATCH /api/projects/{id} (реальный гвард слепка §14.10).
Фронт: FieldWorkspaceScreen/FieldCompetitorScreen/FieldProposalsScreen/FieldManualCompetitorScreen
+ field-shared.css (Forest) + AutopodborServicesPanel в Биллинге. Дословно по прототипу: подзаголовки,
баннер предложений, баннер правил времени 18:00 МСК, Справочник 2ГИС·Яндекс, статус проекта
5/день·заявки, окна сбора с ценами 300/50 + «что известно», полные формы. Пункт меню «Конкурентное поле».
Тесты: backend автоподбор 80/80, фронт автоподбор 49/49. Движок шага 2 = заглушка FakeCompetitorAgent.
OmegaDemoFieldSeeder — только для визуальной проверки (НЕ на прод).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
136 lines
5.6 KiB
TypeScript
136 lines
5.6 KiB
TypeScript
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
import { mount } from '@vue/test-utils';
|
|
import { createPinia, setActivePinia } from 'pinia';
|
|
import { createVuetify } from 'vuetify';
|
|
import { reactive, ref } from 'vue';
|
|
|
|
vi.mock('../../resources/js/api/autopodbor');
|
|
import FieldWorkspaceScreen from '../../resources/js/views/autopodbor/screens/FieldWorkspaceScreen.vue';
|
|
import { useAutopodborStore } from '../../resources/js/stores/autopodborStore';
|
|
|
|
const vuetify = createVuetify();
|
|
|
|
function makeNav() {
|
|
return { go: vi.fn(), ctx: reactive({ competitorId: null }), screen: ref('field') };
|
|
}
|
|
|
|
function field(over: Partial<any> = {}) {
|
|
return {
|
|
id: 1,
|
|
name: 'Окна Комфорт',
|
|
description: 'd',
|
|
is_federal: false,
|
|
relevance_pct: 90,
|
|
origin: 'auto',
|
|
box: 'field',
|
|
site_url: 'okna.ru',
|
|
directory_urls: [],
|
|
studied_at: null,
|
|
study_run_id: null,
|
|
search_run_id: 5,
|
|
counters: { sources: 2, projects_created: 1, projects_in_work: 1 },
|
|
sources: [],
|
|
...over,
|
|
};
|
|
}
|
|
|
|
function mountWs(nav: any) {
|
|
return mount(FieldWorkspaceScreen, { global: { plugins: [vuetify], provide: { autopodborNav: nav } } });
|
|
}
|
|
|
|
describe('FieldWorkspaceScreen', () => {
|
|
beforeEach(() => {
|
|
setActivePinia(createPinia());
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it('грузит поле и показывает конкурентов со счётчиками', async () => {
|
|
const store = useAutopodborStore();
|
|
vi.spyOn(store, 'loadField').mockImplementation(async () => {
|
|
store.field = [field()] as any;
|
|
});
|
|
const w = mountWs(makeNav());
|
|
await new Promise((r) => setTimeout(r, 0));
|
|
expect(store.loadField).toHaveBeenCalled();
|
|
expect(w.text()).toContain('Окна Комфорт');
|
|
expect(w.text()).toContain('создано проектов');
|
|
});
|
|
|
|
it('сортирует по похожести: 100% сверху', async () => {
|
|
const store = useAutopodborStore();
|
|
vi.spyOn(store, 'loadField').mockImplementation(async () => {
|
|
store.field = [
|
|
field({ id: 1, name: 'Низкая', relevance_pct: 40 }),
|
|
field({ id: 2, name: 'Высокая', relevance_pct: 100 }),
|
|
] as any;
|
|
});
|
|
const w = mountWs(makeNav());
|
|
await new Promise((r) => setTimeout(r, 0));
|
|
const names = w.findAll('.ld-card__nm').map((n) => n.text());
|
|
expect(names[0]).toContain('Высокая');
|
|
});
|
|
|
|
it('пустое поле показывает заглушку', async () => {
|
|
const store = useAutopodborStore();
|
|
vi.spyOn(store, 'loadField').mockImplementation(async () => {
|
|
store.field = [] as any;
|
|
});
|
|
const w = mountWs(makeNav());
|
|
await new Promise((r) => setTimeout(r, 0));
|
|
expect(w.text()).toContain('В поле пока пусто');
|
|
});
|
|
|
|
it('«Собрать конкурентов для меня» открывает окно сбора с ценой', async () => {
|
|
const store = useAutopodborStore();
|
|
vi.spyOn(store, 'loadField').mockResolvedValue();
|
|
store.prices = { search: '300', study: '50' };
|
|
const w = mountWs(makeNav());
|
|
await new Promise((r) => setTimeout(r, 0));
|
|
const btn = w.findAll('button').find((b) => b.text().includes('Собрать конкурентов для меня'));
|
|
await btn!.trigger('click');
|
|
expect(w.find('.ld-ovl').exists()).toBe(true);
|
|
expect(w.text()).toContain('Сбор конкурентов');
|
|
expect(w.text()).toContain('300 ₽');
|
|
});
|
|
|
|
it('«Открыть конкурента» открывает карточку', async () => {
|
|
const store = useAutopodborStore();
|
|
vi.spyOn(store, 'loadField').mockImplementation(async () => {
|
|
store.field = [field({ id: 7 })] as any;
|
|
});
|
|
const nav = makeNav();
|
|
const w = mountWs(nav);
|
|
await new Promise((r) => setTimeout(r, 0));
|
|
const btn = w.findAll('button').find((b) => b.text().includes('Открыть конкурента'));
|
|
await btn!.trigger('click');
|
|
expect(nav.ctx.competitorId).toBe(7);
|
|
expect(nav.go).toHaveBeenCalledWith('fieldcompetitor');
|
|
});
|
|
|
|
it('всплывающая панель показывается при ≥2 выбранных и массово включает проекты', async () => {
|
|
const store = useAutopodborStore();
|
|
vi.spyOn(store, 'loadField').mockImplementation(async () => {
|
|
store.field = [
|
|
field({ id: 1, sources: [{ id: 10, project: { id: 100, is_active: false } }], counters: { sources: 1, projects_created: 1, projects_in_work: 0 } }),
|
|
field({ id: 2, sources: [{ id: 11, project: { id: 101, is_active: false } }], counters: { sources: 1, projects_created: 1, projects_in_work: 0 } }),
|
|
] as any;
|
|
});
|
|
const toggleSpy = vi.spyOn(store, 'toggleProjectActive').mockResolvedValue();
|
|
const w = mountWs(makeNav());
|
|
await new Promise((r) => setTimeout(r, 0));
|
|
|
|
const boxes = w.findAll('.ld-pick');
|
|
await boxes[0].trigger('change');
|
|
await boxes[1].trigger('change');
|
|
|
|
expect(w.find('.ld-bulkbar').exists()).toBe(true);
|
|
|
|
const btn = w.findAll('.ld-bulkbar button').find((b) => b.text().includes('Включить'));
|
|
await btn!.trigger('click');
|
|
await new Promise((r) => setTimeout(r, 0));
|
|
|
|
expect(toggleSpy).toHaveBeenCalledWith(100, true);
|
|
expect(toggleSpy).toHaveBeenCalledWith(101, true);
|
|
});
|
|
});
|