Files
portal/app/tests/Frontend/autopodborStore.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

158 lines
7.4 KiB
TypeScript

import { describe, it, expect, beforeEach, vi } from 'vitest';
import { setActivePinia, createPinia } from 'pinia';
vi.mock('../../resources/js/api/autopodbor');
import * as api from '../../resources/js/api/autopodbor';
import { useAutopodborStore } from '../../resources/js/stores/autopodborStore';
describe('autopodborStore', () => {
beforeEach(() => {
setActivePinia(createPinia());
vi.clearAllMocks();
});
it('loadState заполняет enabled/prices/runs', async () => {
(api.fetchState as ReturnType<typeof vi.fn>).mockResolvedValue({
enabled: true,
prices: { search: '500', study: '300' },
runs: [{ id: 1, kind: 'search', status: 'done' }],
});
const s = useAutopodborStore();
await s.loadState();
expect(s.enabled).toBe(true);
expect(s.prices.search).toBe('500');
expect(s.runs.length).toBe(1);
});
it('search кладёт currentRun', async () => {
(api.startSearch as ReturnType<typeof vi.fn>).mockResolvedValue({ id: 9, kind: 'search', status: 'queued' });
const s = useAutopodborStore();
await s.search({ region_code: 16, examples: ['okna.ru'], about_self: [], include_federal: true });
expect(api.startSearch).toHaveBeenCalled();
expect(s.currentRun?.id).toBe(9);
});
it('loadCompetitor кладёт competitor и sources', async () => {
(api.fetchCompetitor as ReturnType<typeof vi.fn>).mockResolvedValue({
competitor: { id: 3, name: 'Окна' },
sources: [{ id: 1, signal_type: 'site' }],
});
const s = useAutopodborStore();
await s.loadCompetitor(3);
expect(s.competitor?.id).toBe(3);
expect(s.sources.length).toBe(1);
});
it('pollRun опрашивает до терминального статуса', async () => {
vi.useFakeTimers();
(api.fetchRun as ReturnType<typeof vi.fn>)
.mockResolvedValueOnce({ id: 5, kind: 'search', status: 'running' })
.mockResolvedValueOnce({ id: 5, kind: 'search', status: 'done' });
const s = useAutopodborStore();
const p = s.pollRun(5);
// прокрутить таймеры и микрозадачи
await vi.runAllTimersAsync();
const final = await p;
expect(final.status).toBe('done');
expect(s.currentRun?.status).toBe('done');
vi.useRealTimers();
});
it('makeProjects возвращает созданные проекты', async () => {
(api.createProjects as ReturnType<typeof vi.fn>).mockResolvedValue([{ id: 1, name: 'Окна Комфорт' }]);
const s = useAutopodborStore();
const res = await s.makeProjects({
source_ids: [1],
regions: [16],
daily_limit_target: 20,
delivery_days_mask: 127,
launch: false,
});
expect(res).toHaveLength(1);
});
// ——— «Конкурентное поле»: рабочее место (два ящика) ———
it('loadField кладёт конкурентов поля', async () => {
(api.fetchField as ReturnType<typeof vi.fn>).mockResolvedValue([
{ id: 7, name: 'Окна', box: 'field', counters: { sources: 2, projects_created: 1, projects_in_work: 1 }, sources: [] },
]);
const s = useAutopodborStore();
await s.loadField();
expect(s.field).toHaveLength(1);
expect(s.field[0].counters.projects_in_work).toBe(1);
});
it('moveCompetitorToBox в proposal убирает конкурента из поля', async () => {
(api.fetchField as ReturnType<typeof vi.fn>).mockResolvedValue([
{ id: 7, name: 'Окна', box: 'field', counters: { sources: 0, projects_created: 0, projects_in_work: 0 }, sources: [] },
]);
(api.setCompetitorBox as ReturnType<typeof vi.fn>).mockResolvedValue({ id: 7, box: 'proposal' });
const s = useAutopodborStore();
await s.loadField();
await s.moveCompetitorToBox(7, 'proposal');
expect(api.setCompetitorBox).toHaveBeenCalledWith(7, 'proposal');
expect(s.field.find((c) => c.id === 7)).toBeUndefined();
});
it('removeCompetitor убирает конкурента из поля', async () => {
(api.fetchField as ReturnType<typeof vi.fn>).mockResolvedValue([
{ id: 7, name: 'Окна', box: 'field', counters: { sources: 0, projects_created: 0, projects_in_work: 0 }, sources: [] },
]);
(api.deleteCompetitor as ReturnType<typeof vi.fn>).mockResolvedValue(undefined);
const s = useAutopodborStore();
await s.loadField();
await s.removeCompetitor(7);
expect(api.deleteCompetitor).toHaveBeenCalledWith(7);
expect(s.field).toHaveLength(0);
});
it('editCompetitor обновляет поля конкурента на месте', async () => {
(api.fetchField as ReturnType<typeof vi.fn>).mockResolvedValue([
{ id: 7, name: 'Старое', box: 'field', counters: { sources: 0, projects_created: 0, projects_in_work: 0 }, sources: [] },
]);
(api.updateCompetitor as ReturnType<typeof vi.fn>).mockResolvedValue({ id: 7, name: 'Новое', relevance_pct: 88 });
const s = useAutopodborStore();
await s.loadField();
await s.editCompetitor(7, { name: 'Новое', relevance_pct: 88 });
expect(s.field[0].name).toBe('Новое');
expect(s.field[0].relevance_pct).toBe(88);
});
it('addFieldCompetitor добавляет нового конкурента в поле', async () => {
(api.createManualCompetitor as ReturnType<typeof vi.fn>).mockResolvedValue({
id: 99, name: 'Ромашка', box: 'field', origin: 'manual',
});
const s = useAutopodborStore();
const c = await s.addFieldCompetitor({ name: 'Ромашка' });
expect(c.id).toBe(99);
expect(s.field.find((x) => x.id === 99)?.name).toBe('Ромашка');
});
it('changeProjectSource зовёт ручку проектов и возвращает сообщение', async () => {
(api.changeProjectSource as ReturnType<typeof vi.fn>).mockResolvedValue({
source_change_message: 'Лиды по старому источнику придут до 30.06, дальше — по новому.',
});
const s = useAutopodborStore();
const res = await s.changeProjectSource(100, 'new.ru');
expect(api.changeProjectSource).toHaveBeenCalledWith(100, 'new.ru');
expect(res.source_change_message).toContain('по новому');
});
it('removeSource убирает источник из карточки конкурента в поле', async () => {
(api.fetchField as ReturnType<typeof vi.fn>).mockResolvedValue([
{
id: 7, name: 'Окна', box: 'field',
counters: { sources: 1, projects_created: 0, projects_in_work: 0 },
sources: [{ id: 50, competitor_id: 7, signal_type: 'site', identifier: 'a.ru', box: 'field', project: null }],
},
]);
(api.deleteSource as ReturnType<typeof vi.fn>).mockResolvedValue(undefined);
const s = useAutopodborStore();
await s.loadField();
await s.removeSource(7, 50);
expect(api.deleteSource).toHaveBeenCalledWith(50);
expect(s.field[0].sources).toHaveLength(0);
});
});