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>
This commit is contained in:
@@ -41,6 +41,11 @@ describe('FieldWorkspaceScreen — «Добавить вручную» объя
|
||||
vi.spyOn(store, 'loadField').mockImplementation(async () => {
|
||||
store.field = [] as never;
|
||||
});
|
||||
// Экран при открытии тянет ДВА списка сразу (см. reload() в FieldWorkspaceScreen).
|
||||
// Без этой заглушки тест физически слал запрос наружу на /api/autopodbor/proposals
|
||||
// и всё равно был зелёным — ошибка вылетала мимо проверок. Ловит сторож сети
|
||||
// в tests/Frontend/setup.ts.
|
||||
vi.spyOn(store, 'loadProposalGroups').mockResolvedValue(undefined);
|
||||
vi.spyOn(store, 'addFieldCompetitor').mockRejectedValue({
|
||||
isAxiosError: true, // как настоящая ошибка axios
|
||||
response: {
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
|
||||
// Карточка на открытии спрашивает дату последней смены пароля. Без заглушки тест
|
||||
// физически ходил в сеть на /api/account/security и всё равно был зелёным: отказ
|
||||
// глушился своим catch. Отдаём «пароль не менялся» — ровно то, что видел тест раньше.
|
||||
vi.mock('../../resources/js/api/account', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import('../../resources/js/api/account')>()),
|
||||
getAccountSecurity: async () => ({ last_password_change_at: null, sessions: [] }),
|
||||
}));
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { createVuetify } from 'vuetify';
|
||||
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
// Экраны тянут статусы воронки с сервера. Без заглушки тест физически ходил в сеть на
|
||||
// /api/lead-statuses и всё равно был зелёным: хранилище глушит отказ своим catch и
|
||||
// остаётся на снимке из кода. Ловит сторож сети в tests/Frontend/setup.ts.
|
||||
vi.mock('../../resources/js/api/leadStatuses', async () => {
|
||||
const { otvetStatusovVoronki } = await import('./support/fixtures');
|
||||
return { listLeadStatuses: async () => otvetStatusovVoronki() };
|
||||
});
|
||||
import { mount, flushPromises } from '@vue/test-utils';
|
||||
import { createVuetify } from 'vuetify';
|
||||
import { createRouter, createMemoryHistory } from 'vue-router';
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import { describe, it, expect, vi, afterEach } from 'vitest';
|
||||
|
||||
// Экран тянет статусы воронки с сервера. Без заглушки тест физически ходил в сеть на
|
||||
// /api/lead-statuses и всё равно был зелёным: хранилище глушит отказ своим catch и
|
||||
// остаётся на снимке из кода. Ловит сторож сети в tests/Frontend/setup.ts.
|
||||
vi.mock('../../resources/js/api/leadStatuses', async () => {
|
||||
const { otvetStatusovVoronki } = await import('./support/fixtures');
|
||||
return { listLeadStatuses: async () => otvetStatusovVoronki() };
|
||||
});
|
||||
import { mount, flushPromises } from '@vue/test-utils';
|
||||
import { createVuetify } from 'vuetify';
|
||||
import { createRouter, createMemoryHistory } from 'vue-router';
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
|
||||
// Окно «реквизиты» на открытии читает уже сохранённые. Без заглушки тест физически
|
||||
// ходил в сеть на /api/tenant/requisites и всё равно был зелёным: экран нарочно
|
||||
// fail-open и глушит отказ. `null` = пустая форма — ровно то, что видел тест раньше.
|
||||
vi.mock('../../resources/js/api/requisites', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import('../../resources/js/api/requisites')>()),
|
||||
getRequisites: async () => null,
|
||||
}));
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { setActivePinia, createPinia } from 'pinia';
|
||||
import { AxiosError } from 'axios';
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
|
||||
// Экран тянет статусы воронки с сервера. Без заглушки тест физически ходил в сеть на
|
||||
// /api/lead-statuses и всё равно был зелёным: хранилище глушит отказ своим catch и
|
||||
// остаётся на снимке из кода. Ловит сторож сети в tests/Frontend/setup.ts.
|
||||
vi.mock('../../resources/js/api/leadStatuses', async () => {
|
||||
const { otvetStatusovVoronki } = await import('./support/fixtures');
|
||||
return { listLeadStatuses: async () => otvetStatusovVoronki() };
|
||||
});
|
||||
import { mount, flushPromises } from '@vue/test-utils';
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
import { createVuetify } from 'vuetify';
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
// Экран тянет статусы воронки с сервера. Без заглушки тест физически ходил в сеть на
|
||||
// /api/lead-statuses и всё равно был зелёным: хранилище глушит отказ своим catch и
|
||||
// остаётся на снимке из кода. Ловит сторож сети в tests/Frontend/setup.ts.
|
||||
vi.mock('../../resources/js/api/leadStatuses', async () => {
|
||||
const { otvetStatusovVoronki } = await import('./support/fixtures');
|
||||
return { listLeadStatuses: async () => otvetStatusovVoronki() };
|
||||
});
|
||||
import { mount, flushPromises } from '@vue/test-utils';
|
||||
import { createVuetify } from 'vuetify';
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
|
||||
@@ -1,4 +1,23 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
|
||||
// Вкладки настроек на открытии тянут свои данные. Без заглушек тест физически ходил
|
||||
// в сеть на /api/account/security, /api/api-keys и /api/tenants/me/webhook-settings —
|
||||
// и всё равно был зелёным: отказы глушились своими catch. Отдаём «пусто», ровно то
|
||||
// состояние, которое тест видел раньше. Ловит сторож сети в tests/Frontend/setup.ts.
|
||||
vi.mock('../../resources/js/api/account', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import('../../resources/js/api/account')>()),
|
||||
getAccountSecurity: async () => ({ last_password_change_at: null, sessions: [] }),
|
||||
}));
|
||||
|
||||
vi.mock('../../resources/js/api/apiKeys', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import('../../resources/js/api/apiKeys')>()),
|
||||
listApiKeys: async () => [],
|
||||
}));
|
||||
|
||||
vi.mock('../../resources/js/api/webhooks', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import('../../resources/js/api/webhooks')>()),
|
||||
getWebhookSettings: async () => null,
|
||||
}));
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { createPinia } from 'pinia';
|
||||
import { createVuetify } from 'vuetify';
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
// Vitest setup для Vuetify в JSDOM-среде.
|
||||
// Vuetify-компоненты используют ResizeObserver/IntersectionObserver/matchMedia,
|
||||
// которые отсутствуют в JSDOM. Добавляем минимальные stub'ы.
|
||||
//
|
||||
// 🔴 Плюс запрет на выход в сеть — см. блок «Сторож сети» ниже.
|
||||
|
||||
import { afterEach, vi } from 'vitest';
|
||||
|
||||
class ResizeObserverStub {
|
||||
observe(): void {}
|
||||
@@ -49,6 +53,122 @@ if (!window.CSS || !window.CSS.supports) {
|
||||
});
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Сторож сети: тест не имеет права ходить наружу по-настоящему.
|
||||
//
|
||||
// 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', {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { CompetitorDto, FieldCompetitorDto, FieldSourceDto, RunDto, SourceProjectDto } from '../../../resources/js/api/autopodbor';
|
||||
import type { ApiLeadStatus } from '../../../resources/js/api/leadStatuses';
|
||||
import type { AutopodborCtx, AutopodborNav, ScreenName } from '../../../resources/js/views/autopodbor/nav';
|
||||
import { LEAD_STATUSES } from '../../../resources/js/composables/leadStatuses';
|
||||
import { reactive, ref } from 'vue';
|
||||
import { vi } from 'vitest';
|
||||
|
||||
@@ -114,6 +116,23 @@ export function navCtx(over: Partial<AutopodborCtx> = {}): AutopodborCtx {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Ответ `GET /api/lead-statuses` — ровно те же пять системных статусов, что лежат
|
||||
* снимком в коде (`LEAD_STATUSES`). Экраны «Сделки» и «Канбан» тянут их на открытии;
|
||||
* до 02.08.2026 запрос уходил в сеть по-настоящему, падал, и хранилище молча
|
||||
* оставалось на снимке. Заглушка отдаёт то же самое, только честно.
|
||||
*/
|
||||
export function otvetStatusovVoronki(): ApiLeadStatus[] {
|
||||
return LEAD_STATUSES.map((s) => ({
|
||||
slug: s.slug,
|
||||
name_ru: s.nameRu,
|
||||
is_system: s.isSystem,
|
||||
sort_order: s.sortOrder,
|
||||
color_hex: s.colorHex,
|
||||
description: null,
|
||||
}));
|
||||
}
|
||||
|
||||
/** Навигация-заглушка: `go` — шпион, состояние полное. */
|
||||
export function navStub(screen: ScreenName = 'entry', ctxOver: Partial<AutopodborCtx> = {}): AutopodborNav {
|
||||
return { go: vi.fn(), ctx: navCtx(ctxOver), screen: ref<ScreenName>(screen) };
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
|
||||
// Этот файл проверяет САМ счётчик посещений, поэтому общая заглушка из setup.ts
|
||||
// здесь снимается — иначе тест проверял бы заглушку вместо рабочего кода.
|
||||
// Выход в сеть тут закрыт своей подменой `fetch` ниже.
|
||||
vi.unmock('../../resources/js/plugins/track');
|
||||
|
||||
describe('свой учёт посетителей', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
|
||||
@@ -106,13 +106,38 @@ PSR_v1, Tooling, CLAUDE.md). Файл `Tooling_v8_3.md` огромный, пра
|
||||
два экрана (`FieldCompetitorScreen.vue`, `NewProjectDialog.vue`) и три теста рекламы.
|
||||
Записи не мешают — хук проверку типов не гоняет.
|
||||
|
||||
### Е. 🌐 Тест ходит в настоящую сеть
|
||||
### Е. ✅ ЗАКРЫТО 02.08.2026 — тесты интерфейса ходили в настоящую сеть
|
||||
|
||||
В полном прогоне тестов интерфейса печатается сетевая ошибка `ERR_NETWORK` на
|
||||
`/api/autopodbor/proposals` из `tests/Frontend/AutopodborManualCompetitorError.spec.ts`.
|
||||
Тест при этом зелёный, потому что ошибка вылетает мимо проверок. Это **тот же класс**, что
|
||||
разбирали 01.08 на PHP-тестах: тест физически ходит наружу. Здесь адрес относительный, так
|
||||
что до интернета не доходит, — но запрос не заглушен, и это стоит закрыть.
|
||||
**Переделывать не надо.**
|
||||
|
||||
🔴 **Тест был не один.** В промте стоял один файл — тот, у которого ошибка случайно
|
||||
вылезла наружу и напечаталась. Замер сторожем показал **93 теста в 14 файлах**:
|
||||
остальные 92 глушились чужими `catch` внутри рабочего кода и были невидимы вообще.
|
||||
Запрета на выход наружу в тестах интерфейса **не было ни одного за всю историю** —
|
||||
ровно как на PHP до 01.08.
|
||||
|
||||
**Что сделано.** В `app/tests/Frontend/setup.ts` появился сторож сети: подменяет
|
||||
`XMLHttpRequest.send` и `fetch`, обрывает запрос **и** запоминает адрес, а в `afterEach`
|
||||
роняет тест. Вторая половина обязательна: без неё код под тестом проглатывает ошибку
|
||||
своим `catch`, и нарушение снова становится невидимым. Там же заглушены **четыре
|
||||
фоновые точки оболочки**, названные поимённо (счётчик сделок, состояние автоподбора,
|
||||
статус баланса, отметка о посещении) — их не спрашивает ни один тест разметки, а
|
||||
отказ у всех четырёх и так проглатывается. Остальные семь файлов заглушают свои
|
||||
вызовы у себя; общий ответ `GET /api/lead-statuses` лежит в
|
||||
`tests/Frontend/support/fixtures.ts` (`otvetStatusovVoronki`).
|
||||
|
||||
🪤 **Заглушка в `setup.ts` действует на ВСЕ файлы — включая тот, что проверяет сам
|
||||
заглушенный код.** `track.spec.ts` начал проверять заглушку вместо счётчика посещений.
|
||||
Лечение — `vi.unmock` в самом файле, одной строкой и с объяснением.
|
||||
|
||||
🔑 Заглушка обязана повторять **то состояние, которое тест видел раньше** (при отказе
|
||||
сети), иначе чинишь одно и ломаешь другое: пустые реквизиты, «пароль не менялся»,
|
||||
те же пять статусов воронки.
|
||||
|
||||
Приёмка: сторож проверен красным трижды (обычный запрос, запрос с проглоченной
|
||||
ошибкой, `fetch`). Полный набор интерфейса — **233 файла / 1750 тестов / 3 пропущено /
|
||||
0 падений / 0 ошибок**. Линтер интерфейса 0, проверка типов **5** — те же
|
||||
унаследованные, что были до смены.
|
||||
|
||||
### Ж. Индекс памяти
|
||||
|
||||
@@ -175,6 +200,9 @@ PSR_v1, Tooling, CLAUDE.md). Файл `Tooling_v8_3.md` огромный, пра
|
||||
|
||||
- **Долг изоляции тестов** — после всей `tests/Feature` в базе не остаётся ничего лишнего.
|
||||
Сторож — `tools/storozh-chistoty-bazy.mjs`.
|
||||
- **Выход тестов интерфейса в сеть** — закрыт сторожем в `app/tests/Frontend/setup.ts`
|
||||
(§3 Е). Новый тест, который «почему-то не видит данных», сперва проверь на этого
|
||||
сторожа: он называет адрес прямо в тексте падения.
|
||||
- **Ошибка суток по всему порталу** — правило названо в `App\Support\MskBoundary`.
|
||||
- **Показ времени приведён к Москве** — держит `tools/storozh-moskovskogo-vremeni.mjs`.
|
||||
|
||||
@@ -205,6 +233,12 @@ PSR_v1, Tooling, CLAUDE.md). Файл `Tooling_v8_3.md` огромный, пра
|
||||
- 🔴🔑 **Всё, что промт говорит о БОЕВОМ состоянии, — гипотеза, пока не замерена командой.**
|
||||
02.08 промт назвал два указателя на бою и велел снести не тот; на бою он был ОДИН, и
|
||||
снос убил бы единственный. Промт пишется по памяти, боевое живёт своей жизнью.
|
||||
- 🔑 **«Один случай» в промте — тоже не замер.** 02.08 промт называл ОДИН тест, ходящий
|
||||
в сеть; сторож нашёл 93 в 14 файлах. Видно было один — тот, чью ошибку никто не
|
||||
проглотил. **Считай не то, что видно, а то, что поймал датчик.**
|
||||
- 🪤 **Заглушка, поставленная на всех сразу, ломает того, кто проверяет её же предмет.**
|
||||
Общая заглушка в `setup.ts` заставила `track.spec.ts` проверять заглушку вместо кода.
|
||||
Датчик — полный прогон; лечение — `vi.unmock` в одном файле с объяснением.
|
||||
- 🔑 **Приёмка вырезанием обязательна**, обе половины: подложил поломку — остановил;
|
||||
убрал — пропустил. Зелёный прогон без вырезания не доказывает ничего.
|
||||
- 🪤 **Вырезание доказывает что-то, только если вырезаны ВСЕ пути к результату.** 02.08 у
|
||||
|
||||
Reference in New Issue
Block a user