Files
portal/app/tests/Frontend/AutopodborFieldWorkspaceScreen.spec.ts
T
Дмитрий 0af5c5696f chore+fix: разметка документации и «неизвестный тип» в интерфейсе убраны в ноль
РАЗМЕТКА ДОКУМЕНТАЦИИ. Проверка сторожа давала 237 замечаний в 21 файле — стало
0 во всех файлах под учётом git. Пустые строки вокруг списков и заголовков
поправлены автоматом в 12 файлах, два файла пришлось делать руками, потому что
автоправка там навредила:
- в описании починки инцидента номера шагов 1…7 — это ИМЕНА, на них ссылается
  сам текст «из шага 2»; автоправка перенумеровала их в 1,1,1,1,2 и сломала
  ссылки. Вывел ровно этот файл из-под одного правила, объяснение — в самом
  файле, остальные правила действуют.
- в протоколе архитектуры автоправка склеила «R-* / DR-*» в «R-*/ DR-*».
  Вместо этого обозначения взяты в обратные кавычки — смысл сохранён.
Семь файлов не тронуты: они вне учёта git — два в папке второй смены и старые
черновики в корне.

ИНТЕРФЕЙС. «Неизвестный тип» убран целиком: было 123 замечания, стало 0.
- Навигация автоподбора описана ОДИН раз — app/resources/js/views/autopodbor/nav.ts.
  Раньше девять экранов повторяли описание с «неизвестным типом», а ещё три
  описывали его каждый по-своему и не полностью.
- Чтение ошибки от сервера собрано в один помощник — app/resources/js/api/errors.ts,
  с проверками. Он намеренно читает ФОРМУ ответа, а не полагается на axios:
  тесты экранов подсовывают ошибку простым объектом той же формы, и экран обязан
  вести себя в тестах так же, как в бою.
- В тестах вместо «неизвестного типа» — именованный доступ exposed<T> и полные
  заготовки: app/tests/Frontend/support/. Каждый тест теперь объявляет ровно те
  поля, до которых дотягивается, и опечатка в имени снова становится ошибкой,
  а не молчаливым undefined. Заодно сняты 14 построчных отключений правила.
- Разведены задвоенные имена в шаблонах: в таблице сделок в одном шаблоне жили
  ДВЕ разные функции isSelected — одна берёт номер сделки, другая строку таблицы.
- Убрана мёртвая функция dirLabel и три неиспользуемые переменные.

ПРОВЕРКА ТИПОВ: было 7 ошибок, стало 5. Две унаследованные закрылись попутно —
более строгие заготовки вскрыли нехватку обязательного поля elements. Оставшиеся
пять были до меня и не в моих строках.

ПРИЁМКА. Разметка проверена вырезанием: подложил поломку обратно — сторож её
поймал, вернул — снова чисто. Полный набор тестов интерфейса: 233 файла,
1750 тестов, 3 пропущено, 0 падений. Рабочий код на PHP не тронут.

NB: LEFTHOOK_EXCLUDE=cspell — тем же приёмом и по той же причине, что и в записи
e0223de4, которой эти шесть файлов исследования заводились в репозиторий: сырой
внешний материал с чужой терминологией. Замерено: до моих правок сторож
орфографии находил в них ровно столько же слов, сколько после — я не добавил
ни одного. Разметка в этих файлах теперь проходит начисто, отключена ТОЛЬКО
орфография и только на эту запись; остальные семнадцать сторожей отработали.
Решение владельца, спрошено отдельно.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 15:07:24 +03:00

129 lines
5.6 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';
vi.mock('../../resources/js/api/autopodbor');
import FieldWorkspaceScreen from '../../resources/js/views/autopodbor/screens/FieldWorkspaceScreen.vue';
import { useAutopodborStore } from '../../resources/js/stores/autopodborStore';
import type { FieldCompetitorDto } from '../../resources/js/api/autopodbor';
import { AUTOPODBOR_NAV, type AutopodborNav } from '../../resources/js/views/autopodbor/nav';
import { fieldCompetitor, fieldSource, navStub, sourceProject } from './support/fixtures';
const vuetify = createVuetify();
function makeNav(): AutopodborNav {
return navStub('field');
}
function field(over: Partial<FieldCompetitorDto> = {}): FieldCompetitorDto {
return fieldCompetitor({
description: 'd',
relevance_pct: 90,
site_url: 'okna.ru',
search_run_id: 5,
counters: { sources: 2, projects_created: 1, projects_in_work: 1 },
...over,
});
}
function mountWs(nav: AutopodborNav) {
return mount(FieldWorkspaceScreen, { global: { plugins: [vuetify], provide: { [AUTOPODBOR_NAV]: nav } } });
}
describe('FieldWorkspaceScreen', () => {
beforeEach(() => {
setActivePinia(createPinia());
vi.clearAllMocks();
});
it('грузит поле и показывает конкурентов со счётчиками', async () => {
const store = useAutopodborStore();
vi.spyOn(store, 'loadField').mockImplementation(async () => {
store.field = [field()];
});
const w = mountWs(makeNav());
await new Promise((r) => setTimeout(r, 0));
expect(store.loadField).toHaveBeenCalled();
expect(w.text()).toContain('Окна Комфорт');
expect(w.text()).toContain('создано проектов');
});
it('сортирует по похожести: 100% сверху', async () => {
const store = useAutopodborStore();
vi.spyOn(store, 'loadField').mockImplementation(async () => {
store.field = [
field({ id: 1, name: 'Низкая', relevance_pct: 40 }),
field({ id: 2, name: 'Высокая', relevance_pct: 100 }),
];
});
const w = mountWs(makeNav());
await new Promise((r) => setTimeout(r, 0));
const names = w.findAll('.ld-card__nm').map((n) => n.text());
expect(names[0]).toContain('Высокая');
});
it('пустое поле показывает заглушку', async () => {
const store = useAutopodborStore();
vi.spyOn(store, 'loadField').mockImplementation(async () => {
store.field = [];
});
const w = mountWs(makeNav());
await new Promise((r) => setTimeout(r, 0));
expect(w.text()).toContain('В поле пока пусто');
});
it('«Собрать конкурентов для меня» открывает окно сбора с ценой', async () => {
const store = useAutopodborStore();
vi.spyOn(store, 'loadField').mockResolvedValue();
store.prices = { search: '300', study: '50' };
const w = mountWs(makeNav());
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 ₽');
});
it('«Открыть конкурента» открывает карточку', async () => {
const store = useAutopodborStore();
vi.spyOn(store, 'loadField').mockImplementation(async () => {
store.field = [field({ id: 7 })];
});
const nav = makeNav();
const w = mountWs(nav);
await new Promise((r) => setTimeout(r, 0));
const btn = w.findAll('button').find((b) => b.text().includes('Открыть конкурента'));
await btn!.trigger('click');
expect(nav.ctx.competitorId).toBe(7);
expect(nav.go).toHaveBeenCalledWith('fieldcompetitor');
});
it('всплывающая панель показывается при ≥2 выбранных и массово включает проекты', async () => {
const store = useAutopodborStore();
vi.spyOn(store, 'loadField').mockImplementation(async () => {
store.field = [
field({ id: 1, sources: [fieldSource({ id: 10, project: sourceProject({ id: 100 }) })], counters: { sources: 1, projects_created: 1, projects_in_work: 0 } }),
field({ id: 2, sources: [fieldSource({ id: 11, project: sourceProject({ id: 101 }) })], counters: { sources: 1, projects_created: 1, projects_in_work: 0 } }),
];
});
const toggleSpy = vi.spyOn(store, 'toggleProjectActive').mockResolvedValue();
const w = mountWs(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(toggleSpy).toHaveBeenCalledWith(100, true);
expect(toggleSpy).toHaveBeenCalledWith(101, true);
});
});