Files
portal/app/tests/Frontend/AdminAutopodborPricingView.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

102 lines
4.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { mount } from '@vue/test-utils';
import { createVuetify } from 'vuetify';
vi.mock('../../resources/js/api/admin');
import AdminAutopodborPricingView from '../../resources/js/views/admin/AdminAutopodborPricingView.vue';
import { listSystemSettings, updateSystemSetting, getPricingTiers } from '../../resources/js/api/admin';
import type { PricingTiersResponse, SystemSetting } from '../../resources/js/api/admin';
import { exposed } from './support/exposed';
const vuetify = createVuetify();
/** То, что экран отдаёт наружу через defineExpose и до чего дотягивается этот тест. */
interface PricingVm {
searchPrice: string;
studyPrice: string;
reason: string;
errorMessage: string | null;
save: () => Promise<void>;
}
function settings(search = '300', study = '50'): SystemSetting[] {
return [
{ key: 'autopodbor_price_search_rub', value: search, type: 'decimal', description: null, updated_at: '', updated_by: null },
{ key: 'autopodbor_price_study_rub', value: study, type: 'decimal', description: null, updated_at: '', updated_by: null },
{ key: 'other_key', value: '1', type: 'int', description: null, updated_at: '', updated_by: null },
];
}
function tiers(): PricingTiersResponse {
return {
active: [{ tier_no: 1, leads_in_tier: 100, price_per_lead_kopecks: 50000, effective_from: '2026-06-01' }],
scheduled: {},
};
}
function mountV() {
return mount(AdminAutopodborPricingView, { global: { plugins: [vuetify] } });
}
describe('AdminAutopodborPricingView', () => {
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(listSystemSettings).mockResolvedValue(settings());
vi.mocked(getPricingTiers).mockResolvedValue(tiers());
vi.mocked(updateSystemSetting).mockResolvedValue({
key: 'autopodbor_price_search_rub',
value: '350',
previous_value: '300',
updated_at: '',
message: 'ok',
});
});
it('грузит текущие тарифы доп.услуг из system-settings', async () => {
const w = mountV();
await new Promise((r) => setTimeout(r, 0));
expect(listSystemSettings).toHaveBeenCalled();
expect(exposed<PricingVm>(w).searchPrice).toBe('300');
expect(exposed<PricingVm>(w).studyPrice).toBe('50');
});
it('показывает сетку лидов для справки', async () => {
const w = mountV();
await new Promise((r) => setTimeout(r, 0));
expect(w.text()).toContain('Тариф на лиды');
expect(w.text()).toContain('500'); // 50000 коп = 500 ₽
});
it('сохранение изменённой цены зовёт updateSystemSetting с value и reason', async () => {
const w = mountV();
await new Promise((r) => setTimeout(r, 0));
const vm = exposed<PricingVm>(w);
vm.searchPrice = '350';
await vm.save();
expect(updateSystemSetting).toHaveBeenCalledWith(
'autopodbor_price_search_rub',
expect.objectContaining({ value: '350' }),
);
expect(updateSystemSetting).not.toHaveBeenCalledWith('autopodbor_price_study_rub', expect.anything());
});
it('причина короче 30 символов блокирует сохранение', async () => {
const w = mountV();
await new Promise((r) => setTimeout(r, 0));
const vm = exposed<PricingVm>(w);
vm.searchPrice = '350';
vm.reason = 'мало';
await vm.save();
expect(updateSystemSetting).not.toHaveBeenCalled();
expect(vm.errorMessage).toContain('30');
});
it('без изменений не зовёт сохранение', async () => {
const w = mountV();
await new Promise((r) => setTimeout(r, 0));
const vm = exposed<PricingVm>(w);
await vm.save();
expect(updateSystemSetting).not.toHaveBeenCalled();
expect(vm.errorMessage).toBeTruthy();
});
});