Files
portal/app/tests/Frontend/setup.ts
T
Дмитрий c5ca3c4387 fix,тесты интерфейса: запрет на выход в сеть — течь была не в одном тесте, а в 93
Промт называл один тест, который печатал сетевую ошибку. Сторож нашёл 93 теста
в 14 файлах: остальные 92 глушились чужими catch внутри рабочего кода и были
невидимы вообще. Запрета на выход наружу в тестах интерфейса не было ни одного
за всю историю — тот же класс, что закрыли 01.08 на PHP.

Сторож в app/tests/Frontend/setup.ts делает две вещи, и обе нужны: обрывает
запрос и запоминает адрес, роняя тест в afterEach. Без второй половины код,
который глушит отказ своим catch, снова всё прячет.

Там же поимённо заглушены четыре фоновые точки оболочки — счётчик сделок,
состояние автоподбора, статус баланса, отметка о посещении. Их не спрашивает
ни один тест разметки, а отказ у всех четырёх и так проглатывается. Остальные
семь файлов глушат свои вызовы у себя; общий ответ статусов воронки лежит в
tests/Frontend/support/fixtures.ts.

Заглушки повторяют то состояние, которое тесты видели при отказе сети, поэтому
ни одна проверка не переписывалась. track.spec.ts снимает общую заглушку через
vi.unmock — он проверяет сам заглушенный код.

Приёмка: сторож проверен красным трижды — обычный запрос, запрос с проглоченной
ошибкой, fetch. Полный набор интерфейса 233 файла / 1750 тестов / 3 пропущено /
0 падений / 0 ошибок. Линтер интерфейса 0, проверка типов 5 — те же
унаследованные, что были до смены. Рабочий код не тронут.

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

189 lines
8.4 KiB
TypeScript

// Vitest setup для Vuetify в JSDOM-среде.
// Vuetify-компоненты используют ResizeObserver/IntersectionObserver/matchMedia,
// которые отсутствуют в JSDOM. Добавляем минимальные stub'ы.
//
// 🔴 Плюс запрет на выход в сеть — см. блок «Сторож сети» ниже.
import { afterEach, vi } from 'vitest';
class ResizeObserverStub {
observe(): void {}
unobserve(): void {}
disconnect(): void {}
}
class IntersectionObserverStub {
observe(): void {}
unobserve(): void {}
disconnect(): void {}
takeRecords(): never[] {
return [];
}
root = null;
rootMargin = '';
thresholds: number[] = [];
}
// Глобальные браузерные API заглушки для JSDOM-среды.
(globalThis as unknown as { ResizeObserver: typeof ResizeObserverStub }).ResizeObserver = ResizeObserverStub;
(globalThis as unknown as { IntersectionObserver: typeof IntersectionObserverStub }).IntersectionObserver =
IntersectionObserverStub;
if (!window.matchMedia) {
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: (query: string) => ({
matches: false,
media: query,
onchange: null,
addListener: () => {},
removeListener: () => {},
addEventListener: () => {},
removeEventListener: () => {},
dispatchEvent: () => false,
}),
});
}
// CSS.supports — Vuetify проверяет наличие color-mix.
if (!window.CSS || !window.CSS.supports) {
Object.defineProperty(window, 'CSS', {
writable: true,
value: { supports: () => true },
});
}
// ─────────────────────────────────────────────────────────────────────────────
// Сторож сети: тест не имеет права ходить наружу по-настоящему.
//
// 02.08.2026. До этого дня запрета не было ни одного, и тест
// AutopodborManualCompetitorError.spec.ts физически слал XHR на
// /api/autopodbor/proposals — оставаясь при этом ЗЕЛЁНЫМ, потому что ошибка
// вылетала мимо проверок. Тот же класс разбирали 01.08 на PHP-тестах.
//
// Сторож делает две вещи, и обе нужны:
// 1) обрывает запрос — наружу ничего не уходит;
// 2) ЗАПОМИНАЕТ адрес и роняет тест в afterEach — иначе код под тестом может
// проглотить ошибку своим catch, и нарушение снова станет невидимым.
//
// Правильное лечение в тесте — заглушить вызов (vi.mock на модуль api/*
// или vi.spyOn на метод хранилища), а не отключать сторожа.
// ─────────────────────────────────────────────────────────────────────────────
const setevyeVylazki: string[] = [];
function zapomnitVylazku(adres: string): Error {
setevyeVylazki.push(adres);
return new Error(
`Тест пытается сходить в сеть по-настоящему: ${adres}. ` +
'Заглушите вызов (vi.mock модуля api/* или vi.spyOn на методе хранилища).',
);
}
const rodnoyOpen = window.XMLHttpRequest.prototype.open;
type XhrSAdresom = XMLHttpRequest & { __adresVylazki?: string };
window.XMLHttpRequest.prototype.open = function (
this: XhrSAdresom,
method: string,
url: string | URL,
...ostalnoe: unknown[]
): void {
this.__adresVylazki = `${method} ${String(url)}`;
// rodnoyOpen перегружен, точный тип аргументов здесь не выразить —
// передаём как есть.
(rodnoyOpen as (...a: unknown[]) => void).call(this, method, url, ...ostalnoe);
};
window.XMLHttpRequest.prototype.send = function (this: XhrSAdresom): void {
throw zapomnitVylazku(this.__adresVylazki ?? 'адрес неизвестен');
};
globalThis.fetch = ((vhod: unknown): never => {
const adres =
typeof vhod === 'string'
? vhod
: vhod instanceof URL
? vhod.href
: ((vhod as { url?: string })?.url ?? 'адрес неизвестен');
throw zapomnitVylazku(`fetch ${adres}`);
}) as unknown as typeof globalThis.fetch;
afterEach(() => {
if (setevyeVylazki.length === 0) return;
const spisok = [...new Set(setevyeVylazki)].join(', ');
setevyeVylazki.length = 0;
throw new Error(
`Тест ходил в сеть по-настоящему: ${spisok}. ` +
'Ошибку мог проглотить catch внутри кода — поэтому она поймана здесь, после теста.',
);
});
// ─────────────────────────────────────────────────────────────────────────────
// Фоновые вызовы оболочки — заглушены по умолчанию для ВСЕХ тестов.
//
// Боковое меню (AppSidebar) и шапка (AppLayout) при открытии сами тянут три вещи:
// счётчик сделок на бейдже, состояние автоподбора и статус баланса. Плюс роутер
// шлёт отметку о посещении. Ни один тест разметки про них не спрашивает, и все три
// вызова в рабочем коде обёрнуты в catch — то есть их отказ НЕВИДИМ.
// Именно поэтому 92 теста в 13 файлах годами ходили в сеть по-настоящему и никто
// об этом не знал (замер 02.08.2026).
//
// 🔑 Это НЕ отключение сторожа: заглушены ровно четыре фоновые точки, названные
// поимённо. Любой другой запрос сторож выше по-прежнему ловит и роняет тест.
// Тест, которому нужно другое поведение этих четырёх, объявляет свой vi.mock —
// он побеждает этот.
// ─────────────────────────────────────────────────────────────────────────────
vi.mock('../../resources/js/api/deals', async (importOriginal) => ({
...(await importOriginal<typeof import('../../resources/js/api/deals')>()),
fetchDealsCount: async (): Promise<number> => 0,
}));
vi.mock('../../resources/js/api/autopodbor', async (importOriginal) => ({
...(await importOriginal<typeof import('../../resources/js/api/autopodbor')>()),
fetchState: async () => ({ enabled: false, runs: [], prices: { search: '0.00', study: '0.00' } }),
}));
vi.mock('../../resources/js/api/billing', async (importOriginal) => ({
...(await importOriginal<typeof import('../../resources/js/api/billing')>()),
getBalanceStatus: async () => ({
frozen_by_balance_at: null,
balance_rub: '0.00',
capacity_leads: 0,
required_leads_per_day: 0,
deficit_leads: 0,
deficit_rub: '0.00',
}),
}));
vi.mock('../../resources/js/plugins/track', () => ({
trackVisit: () => {},
trackScreen: () => {},
trackRegisterOpen: () => {},
}));
// visualViewport — VOverlay/v-menu/v-snackbar используют для location strategies.
if (!window.visualViewport) {
Object.defineProperty(window, 'visualViewport', {
writable: true,
value: {
width: 1024,
height: 768,
offsetLeft: 0,
offsetTop: 0,
pageLeft: 0,
pageTop: 0,
scale: 1,
addEventListener: () => {},
removeEventListener: () => {},
},
});
}