793b20a39c
Сверка прототипа с реализацией показала расхождения — закрыты по 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>
133 lines
5.8 KiB
TypeScript
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('карточку конкурента');
|
|
});
|
|
});
|