1b3683c6b1
- адресные сообщения в окнах сбора/изучения (маппер autopodborErrorMessage) - регион по умолчанию = пустой плейсхолдер «выберите регион» - кнопка «Собрать источники» у изучённого конкурента → «Источники собраны» - сквозной дедуп предложений между прогонами (без двойного списания, ретрай цел) - убран захардкоженный admin_user_id с фронта (id ставит бэкенд) - идемпотентный гард в 3 миграции автоподбора (migrate:fresh снова зелёный) - заглушка Агента: +тип 8-800 (tollfree) для полноты эмуляции Тесты: Pest автоподбор 82/82, Vitest 62/62, vite build зелёный. эскейп: фиксируй (авторизовано владельцем) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
200 lines
9.8 KiB
TypeScript
200 lines
9.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 FieldCompetitorScreen from '../../resources/js/views/autopodbor/screens/FieldCompetitorScreen.vue';
|
|
import { useAutopodborStore } from '../../resources/js/stores/autopodborStore';
|
|
|
|
const vuetify = createVuetify();
|
|
|
|
function makeNav(competitorId: number | null = 3) {
|
|
return {
|
|
go: vi.fn(),
|
|
ctx: reactive({ competitorId, editProjectId: null, selectedSourceIds: [] as number[] }),
|
|
screen: ref('fieldcompetitor'),
|
|
};
|
|
}
|
|
|
|
function src(over: Partial<any> = {}) {
|
|
return {
|
|
id: 10,
|
|
competitor_id: 3,
|
|
signal_type: 'site',
|
|
identifier: 'okna.ru',
|
|
phone_kind: null,
|
|
phone_type: null,
|
|
box: 'field',
|
|
provenance_url: null,
|
|
provenance_label: null,
|
|
created_project_id: null,
|
|
project: null,
|
|
...over,
|
|
};
|
|
}
|
|
|
|
function proj(over: Partial<any> = {}) {
|
|
return {
|
|
id: 100, name: 'P', signal_identifier: 'okna.ru', is_active: true, paused_at: null,
|
|
preflight_blocked_at: null, daily_limit_target: 5, delivered_in_month: 0, delivery_days_mask: 127, regions: [24],
|
|
...over,
|
|
};
|
|
}
|
|
|
|
function seed(store: any, sources: any[], comp: Partial<any> = {}) {
|
|
vi.spyOn(store, 'loadCompetitor').mockImplementation(async () => {
|
|
store.competitor = { id: 3, name: 'Окна Комфорт', is_federal: false, relevance_pct: 90, ...comp } as any;
|
|
store.sources = sources as any;
|
|
});
|
|
}
|
|
|
|
function mountFc(nav: any) {
|
|
return mount(FieldCompetitorScreen, { global: { plugins: [vuetify], provide: { autopodborNav: nav } } });
|
|
}
|
|
|
|
describe('FieldCompetitorScreen', () => {
|
|
beforeEach(() => {
|
|
setActivePinia(createPinia());
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it('грузит конкурента и источники в работе', async () => {
|
|
const store = useAutopodborStore();
|
|
seed(store, [src({ id: 10, identifier: 'okna.ru' })]);
|
|
const w = mountFc(makeNav(3));
|
|
await new Promise((r) => setTimeout(r, 0));
|
|
expect(store.loadCompetitor).toHaveBeenCalledWith(3);
|
|
expect(w.text()).toContain('Окна Комфорт');
|
|
expect(w.text()).toContain('okna.ru');
|
|
});
|
|
|
|
it('источник без проекта показывает «Создать проект» и открывает окно создания', async () => {
|
|
const store = useAutopodborStore();
|
|
seed(store, [src({ id: 10, project: null })]);
|
|
const nav = makeNav(3);
|
|
const w = mountFc(nav);
|
|
await new Promise((r) => setTimeout(r, 0));
|
|
const btn = w.findAll('button').find((b) => b.text() === 'Создать проект');
|
|
expect(btn).toBeTruthy();
|
|
await btn!.trigger('click');
|
|
await new Promise((r) => setTimeout(r, 0));
|
|
expect(w.text()).toContain('Создать проект из источника');
|
|
expect(w.text()).toContain('Дни недели приёма');
|
|
});
|
|
|
|
it('активный проект → «Приостановить» зовёт toggleProjectActive(false)', async () => {
|
|
const store = useAutopodborStore();
|
|
seed(store, [src({ id: 10, project: proj({ id: 100, is_active: true }) })]);
|
|
const toggleSpy = vi.spyOn(store, 'toggleProjectActive').mockResolvedValue();
|
|
const w = mountFc(makeNav(3));
|
|
await new Promise((r) => setTimeout(r, 0));
|
|
const btn = w.findAll('button').find((b) => b.text() === 'Приостановить');
|
|
await btn!.trigger('click');
|
|
await new Promise((r) => setTimeout(r, 0));
|
|
expect(toggleSpy).toHaveBeenCalledWith(100, false);
|
|
});
|
|
|
|
it('телефон показывает значок и тип номера', async () => {
|
|
const store = useAutopodborStore();
|
|
seed(store, [src({ id: 11, signal_type: 'call', identifier: '78432001122', phone_kind: 'substitute', phone_type: 'city' })]);
|
|
const w = mountFc(makeNav(3));
|
|
await new Promise((r) => setTimeout(r, 0));
|
|
expect(w.text()).toContain('🎭');
|
|
expect(w.text()).toContain('городской');
|
|
});
|
|
|
|
it('вкладка «Предложения» показывает источники-предложения и переносит «В работу»', async () => {
|
|
const store = useAutopodborStore();
|
|
seed(store, [src({ id: 12, box: 'proposal', identifier: 'prop.ru' })]);
|
|
const moveSpy = vi.spyOn(store, 'moveSourceToBox').mockResolvedValue();
|
|
const w = mountFc(makeNav(3));
|
|
await new Promise((r) => setTimeout(r, 0));
|
|
const propTab = w.findAll('button').find((b) => b.text().includes('Предложения'));
|
|
await propTab!.trigger('click');
|
|
expect(w.text()).toContain('prop.ru');
|
|
const btn = w.findAll('button').find((b) => b.text().includes('В источники'));
|
|
await btn!.trigger('click');
|
|
await new Promise((r) => setTimeout(r, 0));
|
|
expect(moveSpy).toHaveBeenCalledWith(3, 12, 'field');
|
|
});
|
|
|
|
it('источник с проектом показывает живой источник проекта и меняет через change_source', async () => {
|
|
const store = useAutopodborStore();
|
|
seed(store, [
|
|
src({ id: 10, signal_type: 'site', identifier: 'old.ru', project: proj({ id: 100, signal_identifier: 'live.ru', is_active: true }) }),
|
|
]);
|
|
const changeSpy = vi.spyOn(store, 'changeProjectSource').mockResolvedValue({ source_change_message: 'Лиды дойдут.' });
|
|
const w = mountFc(makeNav(3));
|
|
await new Promise((r) => setTimeout(r, 0));
|
|
// карточка показывает источник проекта, а не old.ru
|
|
expect(w.text()).toContain('live.ru');
|
|
|
|
const editBtn = w.findAll('.ld-link').find((b) => b.text().includes('Изменить источник'));
|
|
await editBtn!.trigger('click');
|
|
await new Promise((r) => setTimeout(r, 0));
|
|
expect(w.text()).toContain('Сменить источник?');
|
|
|
|
const input = w.find('.ld-modal input');
|
|
await input.setValue('new.ru');
|
|
// первый клик «Сохранить» — показывает подтверждение
|
|
let save = w.findAll('.ld-modal button').find((b) => b.text() === 'Сохранить');
|
|
await save!.trigger('click');
|
|
await new Promise((r) => setTimeout(r, 0));
|
|
expect(w.text()).toContain('Подтвердите смену источника');
|
|
// второй клик «Сменить источник» — выполняет change_source
|
|
save = w.findAll('.ld-modal button').find((b) => b.text() === 'Сменить источник');
|
|
await save!.trigger('click');
|
|
await new Promise((r) => setTimeout(r, 0));
|
|
expect(changeSpy).toHaveBeenCalledWith(100, 'new.ru');
|
|
});
|
|
|
|
it('массовое «Приостановить выбранные» паузит проекты выбранных источников', async () => {
|
|
const store = useAutopodborStore();
|
|
seed(store, [
|
|
src({ id: 10, project: proj({ id: 100, signal_identifier: 'a.ru', is_active: true }) }),
|
|
src({ id: 11, identifier: 'b.ru', project: proj({ id: 101, signal_identifier: 'b.ru', is_active: true }) }),
|
|
]);
|
|
const toggleSpy = vi.spyOn(store, 'toggleProjectActive').mockResolvedValue();
|
|
const w = mountFc(makeNav(3));
|
|
await new Promise((r) => setTimeout(r, 0));
|
|
const boxes = w.findAll('.ld-pick');
|
|
await boxes[0].trigger('change');
|
|
await boxes[1].trigger('change');
|
|
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, false);
|
|
expect(toggleSpy).toHaveBeenCalledWith(101, false);
|
|
});
|
|
|
|
it('у изучённого конкурента нет кнопки «Собрать источники», показано «Источники собраны»', async () => {
|
|
const store = useAutopodborStore();
|
|
seed(store, [src({ id: 10 })], { studied_at: '2026-06-30T00:00:00+00:00' });
|
|
const w = mountFc(makeNav(3));
|
|
await new Promise((r) => setTimeout(r, 0));
|
|
expect(w.findAll('button').find((b) => b.text().includes('Собрать источники для меня'))).toBeFalsy();
|
|
expect(w.text()).toContain('Источники собраны');
|
|
});
|
|
|
|
it('неизучённый конкурент показывает кнопку «Собрать источники для меня»', async () => {
|
|
const store = useAutopodborStore();
|
|
seed(store, [src({ id: 10 })], { studied_at: null });
|
|
const w = mountFc(makeNav(3));
|
|
await new Promise((r) => setTimeout(r, 0));
|
|
expect(w.findAll('button').find((b) => b.text().includes('Собрать источники для меня'))).toBeTruthy();
|
|
});
|
|
|
|
it('окно «Изменить источник» открывается с залоченным типом', async () => {
|
|
const store = useAutopodborStore();
|
|
seed(store, [src({ id: 10, signal_type: 'site', identifier: 'okna.ru' })]);
|
|
const w = mountFc(makeNav(3));
|
|
await new Promise((r) => setTimeout(r, 0));
|
|
const btn = w.findAll('.ld-link').find((b) => b.text().includes('Изменить источник'));
|
|
await btn!.trigger('click');
|
|
await new Promise((r) => setTimeout(r, 0));
|
|
expect(w.text()).toContain('тип не меняется');
|
|
});
|
|
});
|