Files
portal/app/tests/Frontend/setup.ts
T
Дмитрий 1aca591558 fix(тесты экранов): в главную перенесён запрет на выход в сеть
Перенос ГОТОВОЙ работы соседей: запись c5ca3c438 из ветки
feat/prospects-manual-testing-kp (02.08.2026). Своего такого же не писал — это
задвоило бы правило и устроило столкновение при сведении их ветки.

Зачем. В главной тесты экранов физически ходили в интернет, оставаясь ЗЕЛЁНЫМИ:
ошибка вылетала мимо проверок. Видна была только одна течь — AxiosError на
/api/autopodbor/proposals в общем прогоне; их 93 в 14 файлах.

Сторож делает две вещи, и обе нужны:
  1) обрывает запрос — наружу физически ничего не уходит;
  2) ЗАПОМИНАЕТ адрес и роняет тест в afterEach — иначе код под тестом глотает
     ошибку своим catch, и нарушение снова становится невидимым.

Приёмка ВЫРЕЗАНИЕМ, а не по зелёному: подложен тест, который нарочно лезет в сеть
и глотает ошибку. Сторож покраснел и назвал адрес — то есть вторая половина
работает. Подложка удалена.

Взято не всё: их промт-документ 2026-08-02-PROMT-podgotovka-k-produ-8.md в главную
НЕ переносился. Вспомогательный tests/Frontend/support/fixtures.ts взят — без него
четыре их правленых теста не собираются.

Проверено: 242 файла, 1947 тестов зелёные, 3 пропущены, строки «Errors» больше нет
(была 1). Серверный прогон не затронут.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 09:20:24 +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: () => {},
},
});
}