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

72 lines
2.8 KiB
TypeScript

import { describe, it, expect, beforeEach, vi } from 'vitest';
import { setActivePinia, createPinia } from 'pinia';
import axios from 'axios';
vi.mock('axios');
import { useProjectsStore, type Project } from '../../resources/js/stores/projectsStore';
const sampleProject: Project = {
id: 42,
name: 'Test',
signal_type: 'site' as const,
signal_identifier: 'x.ru',
is_active: false,
daily_limit_target: 5,
delivered_today: 0,
sync_status: 'ok' as const,
preflight_blocked_at: null,
};
describe('projectsStore toggleActive', () => {
beforeEach(() => {
setActivePinia(createPinia());
vi.clearAllMocks();
});
it('при 409 balance_insufficient НЕ делает fetch и возвращает deferred+balance', async () => {
const balancePayload = {
current_balance_rub: '100.00',
current_capacity_leads: 1,
would_be_required_leads: 5,
deficit_leads: 4,
topup_rub: '400.00',
};
(axios.patch as unknown as ReturnType<typeof vi.fn>).mockRejectedValue({
response: { status: 409, data: { error: 'balance_insufficient', balance: balancePayload } },
});
(axios.get as unknown as ReturnType<typeof vi.fn>).mockResolvedValue({
data: { data: [], meta: { total: 0 } },
});
const store = useProjectsStore();
store.items = [{ ...sampleProject }];
const result = await store.toggleActive({ ...sampleProject });
// Стор НЕ должен обновить is_active (fetch не вызывался при 409)
expect(result).toEqual({ deferred: true, balance: balancePayload });
expect(axios.get).not.toHaveBeenCalled(); // fetch НЕ вызван
// is_active в сторе не изменился
expect(store.items[0]?.is_active).toBe(false);
});
it('при успехе (200) делает fetch и возвращает void', async () => {
(axios.patch as unknown as ReturnType<typeof vi.fn>).mockResolvedValue({ data: { data: { id: 42 } } });
(axios.get as unknown as ReturnType<typeof vi.fn>).mockResolvedValue({
data: { data: [{ ...sampleProject, is_active: true }], meta: { total: 1 } },
});
const store = useProjectsStore();
const result = await store.toggleActive({ ...sampleProject });
expect(result).toBeUndefined();
expect(axios.get).toHaveBeenCalled(); // fetch вызван
});
it('другие ошибки (500) пробрасываются', async () => {
(axios.patch as unknown as ReturnType<typeof vi.fn>).mockRejectedValue({
response: { status: 500, data: { error: 'server_error' } },
});
const store = useProjectsStore();
await expect(store.toggleActive({ ...sampleProject })).rejects.toBeDefined();
});
});