Files
portal/app/tests/Frontend/AutopodborFieldCompetitorScreen.spec.ts
T
Дмитрий 1b3683c6b1 fix(конкурентное поле): 6 находок теста «тупого клиента» — ошибки, регион, дедуп, миграции
- адресные сообщения в окнах сбора/изучения (маппер 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>
2026-06-30 06:42:33 +03:00

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('тип не меняется');
});
});