1aca591558
Перенос ГОТОВОЙ работы соседей: запись 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>
189 lines
8.4 KiB
TypeScript
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: () => {},
|
|
},
|
|
});
|
|
}
|