29b90c5759
Клик «Удалить» у предложения открывает окно со всеми предложениями, найденными в том же месте (совпал url карточки/сайта в «где нашли»), с галочками. По умолчанию отмечена только нажатая строка; клиент выбирает, какие связанные строки удалить. «В работе» — прежнее окно. Массовое удаление предложений — подтверждение со списком выбранных. Только показ и порядок «сначала выбери, потом удали»; БД/бэкенд не тронуты. Тесты: пул по url, строгое совпадение, дефолт-галочка, удаление отмеченных. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
244 lines
11 KiB
TypeScript
244 lines
11 KiB
TypeScript
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
import { mount } from '@vue/test-utils';
|
|
import { setActivePinia, createPinia } from 'pinia';
|
|
|
|
vi.mock('../../resources/js/api/autopodbor', async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import('../../resources/js/api/autopodbor')>();
|
|
return { ...actual, fetchCompetitor: vi.fn() };
|
|
});
|
|
import * as api from '../../resources/js/api/autopodbor';
|
|
import type { WhereFound } from '../../resources/js/api/autopodbor';
|
|
import { useAutopodborStore } from '../../resources/js/stores/autopodborStore';
|
|
import FieldCompetitorScreen from '../../resources/js/views/autopodbor/screens/FieldCompetitorScreen.vue';
|
|
|
|
const makeNav = (competitorId = 10) => ({
|
|
go: vi.fn(),
|
|
ctx: { competitorId, editProjectId: null, selectedSourceIds: [] },
|
|
screen: { value: 'field-competitor' },
|
|
});
|
|
|
|
// Источник-предложение (box 'proposal', без проекта) с произвольным списком «где нашли».
|
|
const makeProposal = (id: number, whereFound: WhereFound[] = []) => ({
|
|
id,
|
|
competitor_id: 10,
|
|
signal_type: 'call' as const,
|
|
identifier: `7800100811${id}`,
|
|
phone_kind: 'real' as const,
|
|
phone_type: 'tollfree' as const,
|
|
box: 'proposal' as const,
|
|
provenance_url: null,
|
|
provenance_label: null,
|
|
created_project_id: null,
|
|
project: null,
|
|
where_found: whereFound,
|
|
});
|
|
|
|
// Источник в работе (box 'field', без активного проекта) — окно удаления должно остаться прежним.
|
|
const makeWorkSource = (id: number, whereFound: WhereFound[] = []) => ({
|
|
id,
|
|
competitor_id: 10,
|
|
signal_type: 'site' as const,
|
|
identifier: `work-${id}.ru`,
|
|
phone_kind: null,
|
|
phone_type: null,
|
|
box: 'field' as const,
|
|
provenance_url: null,
|
|
provenance_label: null,
|
|
created_project_id: null,
|
|
project: null,
|
|
where_found: whereFound,
|
|
});
|
|
|
|
async function mountWith(sources: unknown[]) {
|
|
(api.fetchCompetitor as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
competitor: { id: 10, name: 'Окна', is_federal: false, relevance_pct: 90, origin: 'auto', box: 'field', site_url: 'okna.ru', directory_urls: [], studied_at: '2026-01-01', study_run_id: 1, search_run_id: 1, description: '' },
|
|
sources,
|
|
});
|
|
const store = useAutopodborStore();
|
|
await store.loadCompetitor(10);
|
|
const wrapper = mount(FieldCompetitorScreen, {
|
|
global: { provide: { autopodborNav: makeNav() }, stubs: { Transition: false } },
|
|
});
|
|
await new Promise((r) => setTimeout(r, 50));
|
|
return { wrapper, store };
|
|
}
|
|
|
|
// Место = url карточки/сайта.
|
|
const CARD = 'https://2gis.ru/firm/card-A';
|
|
const SITE = 'https://okna-a.ru';
|
|
const OTHER = 'https://2gis.ru/firm/card-B';
|
|
|
|
beforeEach(() => {
|
|
setActivePinia(createPinia());
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
describe('FieldCompetitorScreen — удаление предложения показывает пул связанных строк', () => {
|
|
it('открывает окно-пул со строками из того же места (совпал url)', async () => {
|
|
const { wrapper } = await mountWith([
|
|
makeProposal(1, [{ label: 'карточка 2ГИС', url: CARD }]),
|
|
makeProposal(2, [{ label: 'карточка 2ГИС', url: CARD }]),
|
|
makeProposal(3, [{ label: 'карточка 2ГИС', url: OTHER }]),
|
|
]);
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const vm = wrapper.vm as any;
|
|
vm.switchTab('sugg');
|
|
const clicked = vm.props.find((p: { id: number }) => p.id === 1);
|
|
vm.openDelete(clicked);
|
|
await wrapper.vm.$nextTick();
|
|
|
|
expect(vm.delPool.open).toBe(true);
|
|
const ids = vm.delPool.items.map((i: { src: { id: number } }) => i.src.id).sort();
|
|
expect(ids).toEqual([1, 2]); // 3 — другое место, не в пуле
|
|
});
|
|
|
|
it('по умолчанию отмечена только нажатая строка', async () => {
|
|
const { wrapper } = await mountWith([
|
|
makeProposal(1, [{ label: 'карточка 2ГИС', url: CARD }]),
|
|
makeProposal(2, [{ label: 'карточка 2ГИС', url: CARD }]),
|
|
]);
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const vm = wrapper.vm as any;
|
|
vm.switchTab('sugg');
|
|
vm.openDelete(vm.props.find((p: { id: number }) => p.id === 1));
|
|
await wrapper.vm.$nextTick();
|
|
|
|
const checked = vm.delPool.items.filter((i: { checked: boolean }) => i.checked).map((i: { src: { id: number } }) => i.src.id);
|
|
expect(checked).toEqual([1]);
|
|
});
|
|
|
|
it('строка без общего места показывает только себя', async () => {
|
|
const { wrapper } = await mountWith([
|
|
makeProposal(1, [{ label: 'карточка 2ГИС', url: CARD }]),
|
|
makeProposal(2, [{ label: 'карточка 2ГИС', url: OTHER }]),
|
|
]);
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const vm = wrapper.vm as any;
|
|
vm.switchTab('sugg');
|
|
vm.openDelete(vm.props.find((p: { id: number }) => p.id === 2));
|
|
await wrapper.vm.$nextTick();
|
|
|
|
expect(vm.delPool.items).toHaveLength(1);
|
|
expect(wrapper.find('.ld-del-pool__none').text()).toContain('Других строк, найденных в этом же месте, нет');
|
|
});
|
|
|
|
it('generic-метки без url не связывают строки', async () => {
|
|
const { wrapper } = await mountWith([
|
|
makeProposal(1, [{ label: 'привязана клиентом', url: null }]),
|
|
makeProposal(2, [{ label: 'привязана клиентом', url: null }]),
|
|
]);
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const vm = wrapper.vm as any;
|
|
vm.switchTab('sugg');
|
|
vm.openDelete(vm.props.find((p: { id: number }) => p.id === 1));
|
|
await wrapper.vm.$nextTick();
|
|
|
|
expect(vm.delPool.items).toHaveLength(1);
|
|
});
|
|
|
|
it('строки одного места объединяются даже при разных подписях, если совпал url', async () => {
|
|
const { wrapper } = await mountWith([
|
|
makeProposal(1, [{ label: 'карточка 2ГИС', url: CARD }]),
|
|
makeProposal(2, [{ label: 'сайт', url: SITE }, { label: 'найдено по телефону', url: CARD }]),
|
|
]);
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const vm = wrapper.vm as any;
|
|
vm.switchTab('sugg');
|
|
vm.openDelete(vm.props.find((p: { id: number }) => p.id === 1));
|
|
await wrapper.vm.$nextTick();
|
|
|
|
const ids = vm.delPool.items.map((i: { src: { id: number } }) => i.src.id).sort();
|
|
expect(ids).toEqual([1, 2]);
|
|
});
|
|
|
|
it('подтверждение удаляет только отмеченные строки', async () => {
|
|
const { wrapper, store } = await mountWith([
|
|
makeProposal(1, [{ label: 'карточка 2ГИС', url: CARD }]),
|
|
makeProposal(2, [{ label: 'карточка 2ГИС', url: CARD }]),
|
|
]);
|
|
const spy = vi.spyOn(store, 'removeSource').mockResolvedValue();
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const vm = wrapper.vm as any;
|
|
vm.switchTab('sugg');
|
|
vm.openDelete(vm.props.find((p: { id: number }) => p.id === 1));
|
|
await wrapper.vm.$nextTick();
|
|
// клиент дополнительно отмечает вторую связанную строку
|
|
vm.delPool.items.find((i: { src: { id: number } }) => i.src.id === 2).checked = true;
|
|
await vm.confirmDeletePool();
|
|
|
|
expect(spy).toHaveBeenCalledWith(10, 1);
|
|
expect(spy).toHaveBeenCalledWith(10, 2);
|
|
expect(spy).toHaveBeenCalledTimes(2);
|
|
expect(vm.delPool.open).toBe(false);
|
|
});
|
|
|
|
it('подтверждение с одной галочкой удаляет только её', async () => {
|
|
const { wrapper, store } = await mountWith([
|
|
makeProposal(1, [{ label: 'карточка 2ГИС', url: CARD }]),
|
|
makeProposal(2, [{ label: 'карточка 2ГИС', url: CARD }]),
|
|
]);
|
|
const spy = vi.spyOn(store, 'removeSource').mockResolvedValue();
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const vm = wrapper.vm as any;
|
|
vm.switchTab('sugg');
|
|
vm.openDelete(vm.props.find((p: { id: number }) => p.id === 1));
|
|
await wrapper.vm.$nextTick();
|
|
await vm.confirmDeletePool();
|
|
|
|
expect(spy).toHaveBeenCalledWith(10, 1);
|
|
expect(spy).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('удаление источника «в работе» НЕ открывает пул (старое окно)', async () => {
|
|
const { wrapper } = await mountWith([makeWorkSource(1, [{ label: 'сайт', url: SITE }])]);
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const vm = wrapper.vm as any;
|
|
vm.openDelete(vm.inWork[0]);
|
|
await wrapper.vm.$nextTick();
|
|
|
|
expect(vm.delPool.open).toBe(false);
|
|
expect(vm.del.open).toBe(true);
|
|
expect(wrapper.text()).toContain('будет удалён из работы');
|
|
});
|
|
});
|
|
|
|
describe('FieldCompetitorScreen — окно подтверждения при массовом удалении предложений', () => {
|
|
const L1: WhereFound[] = [{ label: 'карточка 2ГИС', url: CARD }];
|
|
const L2: WhereFound[] = [{ label: 'сайт конкурента', url: SITE }];
|
|
|
|
it('кнопка массового удаления открывает окно и НЕ удаляет сразу', async () => {
|
|
const { wrapper, store } = await mountWith([makeProposal(1, L1), makeProposal(2, L2)]);
|
|
const spy = vi.spyOn(store, 'removeSource').mockResolvedValue();
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const vm = wrapper.vm as any;
|
|
vm.switchTab('sugg');
|
|
vm.selected = [1, 2];
|
|
await wrapper.vm.$nextTick();
|
|
|
|
const delBtn = wrapper.findAll('.ld-bulkbar button').find((b) => b.text().includes('Удалить'))!;
|
|
await delBtn.trigger('click');
|
|
await wrapper.vm.$nextTick();
|
|
|
|
expect(vm.delBulk.open).toBe(true);
|
|
expect(spy).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('подтверждение в окне удаляет каждое предложение через removeSource', async () => {
|
|
const { wrapper, store } = await mountWith([makeProposal(1, L1), makeProposal(2, L2)]);
|
|
const spy = vi.spyOn(store, 'removeSource').mockResolvedValue();
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const vm = wrapper.vm as any;
|
|
vm.switchTab('sugg');
|
|
vm.selected = [1, 2];
|
|
await wrapper.vm.$nextTick();
|
|
vm.askBulkDeleteProps();
|
|
await wrapper.vm.$nextTick();
|
|
|
|
await vm.bulkDeleteProps();
|
|
expect(spy).toHaveBeenCalledWith(10, 1);
|
|
expect(spy).toHaveBeenCalledWith(10, 2);
|
|
expect(spy).toHaveBeenCalledTimes(2);
|
|
expect(vm.delBulk.open).toBe(false);
|
|
});
|
|
});
|