Files
portal/app/tests/Frontend/AutopodborFieldWorkspaceScreen.spec.ts
T
Дмитрий 4387333118 feat(Конкурентное поле): рабочее место конкуренты→источники→проекты (поверх автоподбора)
Фича «Конкурентное поле» на 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>
2026-06-30 04:18:46 +03:00

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);
});
});