Files
portal/app/tests/Frontend/autopodborStore.spec.ts
T
Дмитрий d2f3a171a9 fix(autopodbor): источник исчезает сразу при удалении + формы не закрываются при выделении текста
- стор removeSource чистит store.sources - карточка конкурента обновляется без reload
- 8 оверлеев на экранах Поле/Предложения переведены на useOverlayDismiss

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 18:14:18 +03:00

177 lines
8.6 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);
});
it('removeSource убирает источник и из списка экрана карточки (store.sources) — без перезагрузки', async () => {
// Экран «карточка конкурента» рендерит store.sources (не field[].sources). До фикса
// removeSource обновлял только поле → удалённый источник висел до обновления страницы.
(api.fetchCompetitor as ReturnType<typeof vi.fn>).mockResolvedValue({
competitor: { id: 7, name: 'Окна' },
sources: [
{ id: 50, competitor_id: 7, signal_type: 'site', identifier: 'a.ru', box: 'field', project: null },
{ id: 51, competitor_id: 7, signal_type: 'call', identifier: '79990000000', box: 'field', project: null },
],
});
(api.deleteSource as ReturnType<typeof vi.fn>).mockResolvedValue(undefined);
const s = useAutopodborStore();
await s.loadCompetitor(7);
expect(s.sources).toHaveLength(2);
await s.removeSource(7, 50);
expect(api.deleteSource).toHaveBeenCalledWith(50);
expect(s.sources.map((x) => x.id)).toEqual([51]);
});
});