0af5c5696f
РАЗМЕТКА ДОКУМЕНТАЦИИ. Проверка сторожа давала 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>
102 lines
4.1 KiB
TypeScript
102 lines
4.1 KiB
TypeScript
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();
|
||
});
|
||
});
|