Files
portal/app/tests/Frontend/AutopodborFieldProposalsScreen.spec.ts
T
Дмитрий 793b20a39c feat(конкурентное поле): доводка фронта до прототипа — F1/F2/F3 + чистка M2
Сверка прототипа с реализацией показала расхождения — закрыты по TDD (dev, фронт):

- F1: экран «Предложения» (FieldProposalsScreen) переписан под вид «Поля» —
  карточки-плитки field-shared, тип+«предложение», крупная похожесть, Сайт +
  Справочник 2ГИС·Яндекс, править/удалять в карточке, массовый перенос; кнопка
  «Собрать конкурентов» открывает единое окно сбора 300 ₽ вместо старого autoform.
- F2: новый дружелюбный админ-экран AdminAutopodborPricingView (правка цен
  доп.услуг через PUT /api/admin/system-settings/{key} с обоснованием для аудита,
  сетка лидов для справки) + маршрут /admin/autopodbor-pricing + пункт меню.
- F3: колонка «когда списывается» в панели доп.услуг биллинга.
- M2: удалён мёртвый экран FieldManualCompetitorScreen (+ спека) — на него не
  было переходов; ручное добавление живёт окном на «Поле».

Тесты автоподбор+админ 43/43 зелёные, продакшен-вёрстка eslint-чистая, vite build .
НЕ на проде. M1 (18:00/21:00 МСК) — не баг, реальный инвариант продукта, не трогал.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 04:57:58 +03:00

133 lines
5.8 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 FieldProposalsScreen from '../../resources/js/views/autopodbor/screens/FieldProposalsScreen.vue';
import { useAutopodborStore } from '../../resources/js/stores/autopodborStore';
const vuetify = createVuetify();
function makeNav() {
return { go: vi.fn(), ctx: reactive({ competitorId: null }), screen: ref('field-proposals') };
}
function comp(over: Partial<any> = {}) {
return {
id: 1,
name: 'Окна',
description: 'Окна ПВХ под ключ',
is_federal: false,
relevance_pct: 80,
origin: 'auto',
box: 'proposal',
site_url: 'okna.ru',
directory_urls: ['https://2gis.ru/firm/1', 'https://yandex.ru/maps/1'],
studied_at: null,
study_run_id: null,
search_run_id: 5,
...over,
};
}
function mountP(nav: any) {
return mount(FieldProposalsScreen, { global: { plugins: [vuetify], provide: { autopodborNav: nav } } });
}
describe('FieldProposalsScreen', () => {
beforeEach(() => {
setActivePinia(createPinia());
vi.clearAllMocks();
});
it('грузит предложения и показывает карточку-плитку с похожестью и Справочником', async () => {
const store = useAutopodborStore();
vi.spyOn(store, 'loadProposals').mockImplementation(async () => {
store.proposals = [comp({ id: 1, name: 'Окна Комфорт', relevance_pct: 80 })] as any;
});
const w = mountP(makeNav());
await new Promise((r) => setTimeout(r, 0));
expect(store.loadProposals).toHaveBeenCalled();
expect(w.find('.ld-card').exists()).toBe(true);
expect(w.text()).toContain('Окна Комфорт');
expect(w.text()).toContain('80');
expect(w.text()).toContain('Справочник');
expect(w.text()).toContain('2ГИС');
expect(w.text()).toContain('Яндекс.Карты');
});
it('«В поле →» по конкуренту зовёт moveCompetitorToBox(field)', async () => {
const store = useAutopodborStore();
vi.spyOn(store, 'loadProposals').mockImplementation(async () => {
store.proposals = [comp({ id: 7 })] as any;
});
const moveSpy = vi.spyOn(store, 'moveCompetitorToBox').mockResolvedValue();
const w = mountP(makeNav());
await new Promise((r) => setTimeout(r, 0));
const btn = w.find('.ld-cfoot button');
await btn.trigger('click');
await new Promise((r) => setTimeout(r, 0));
expect(moveSpy).toHaveBeenCalledWith(7, 'field');
});
it('пусто показывает заглушку', async () => {
const store = useAutopodborStore();
vi.spyOn(store, 'loadProposals').mockImplementation(async () => {
store.proposals = [] as any;
});
const w = mountP(makeNav());
await new Promise((r) => setTimeout(r, 0));
expect(w.text()).toContain('Предложений пока нет');
});
it('«Собрать конкурентов» открывает окно сбора с ценой 300 ₽ (не уходит на старую форму)', async () => {
const store = useAutopodborStore();
vi.spyOn(store, 'loadProposals').mockResolvedValue();
store.prices = { search: '300', study: '50' };
const nav = makeNav();
const w = mountP(nav);
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 ₽');
expect(nav.go).not.toHaveBeenCalledWith('autoform');
});
it('массово переносит выбранных в поле при ≥2', async () => {
const store = useAutopodborStore();
vi.spyOn(store, 'loadProposals').mockImplementation(async () => {
store.proposals = [comp({ id: 1 }), comp({ id: 2 })] as any;
});
const moveSpy = vi.spyOn(store, 'moveCompetitorToBox').mockResolvedValue();
const w = mountP(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(moveSpy).toHaveBeenCalledWith(1, 'field');
expect(moveSpy).toHaveBeenCalledWith(2, 'field');
});
it('«Изменить» открывает окно правки карточки конкурента', async () => {
const store = useAutopodborStore();
vi.spyOn(store, 'loadProposals').mockImplementation(async () => {
store.proposals = [comp({ id: 1, name: 'Окна Комфорт' })] as any;
});
const w = mountP(makeNav());
await new Promise((r) => setTimeout(r, 0));
const link = w.findAll('.ld-link').find((b) => b.text().includes('Изменить'));
await link!.trigger('click');
await new Promise((r) => setTimeout(r, 0));
expect(w.find('.ld-ovl').exists()).toBe(true);
expect(w.text()).toContain('карточку конкурента');
});
});