Как делать11 октября 2026 г.8 мин чтения
ReactШаг 7 из 8

Тестирование фронтенда: Vitest и Testing Library

Коротко

Фронтенд тестируют на двух уровнях: чистые функции — обычными unit-тестами в Vitest, компоненты — через Testing Library так, как ими пользуется человек: находят элементы по ролям и подписям, кликают и печатают через user-event и проверяют то, что видно на экране. Сеть в таких тестах подменяют — через vi.stubGlobal для fetch или через MSW на уровне сетевых запросов. Хороший тест падает, когда сломалось поведение, и молчит, когда вы просто переписали код внутри компонента.

Пошаговый урокУровень: СреднийТестирование frontendПрактика: ~1,5–3 ч

После материала вы сможете

  • написать unit-тест чистой функции в Vitest и убедиться, что он ловит намеренную ошибку
  • протестировать React-компонент через поведение: найти элементы по роли и подписи, кликнуть и ввести текст через user-event
  • подменить сетевой запрос через vi.stubGlobal и vi.fn или через MSW и проверить сценарии успеха и ошибки
  • отличить устойчивый тест от хрупкого и объяснить, почему getByRole лучше поиска по классам

Что знать заранее

Проверено на

Vitest 5.0@testing-library/react 16.3@testing-library/user-event 14.6@testing-library/jest-dom 7.0MSW 3.0React 19.3Vite 8.3jsdom 30.1Node.js 24 LTS (24.18)

Сверено с документацией: 11 октября 2026 г.

Содержание
  1. 01Что и зачем тестировать на фронтенде
  2. 02Установка: Vitest, jsdom и Testing Library
  3. 03Тест чистой функции
  4. 04Компонент через поведение: роли и user-event
  5. 05Моки сети: vi.fn и MSW
  6. 06Что не тестировать и как не делать хрупкие тесты
  7. 07Упражнения

Что и зачем тестировать на фронтенде

Автоматический тест — это код, который запускает ваш код и сравнивает результат с ожидаемым. Его ценность в одном: он ловит регрессию — поломку того, что уже работало, — раньше пользователя. Цена тоже одна: тесты надо поддерживать. Поэтому вопрос не «как покрыть всё», а «какие тесты дают уверенность за разумную цену».

Unit-тест

Что проверяет
Чистую функцию: расчёт корзины, форматирование телефона, валидацию
Инструмент в этом уроке
Vitest

Тест компонента

Что проверяет
Компонент в DOM: что видно, что происходит после клика и ввода, что ушло в сеть
Инструмент в этом уроке
Vitest + jsdom + Testing Library + user-event

End-to-end

Что проверяет
Сценарий в настоящем браузере на собранном приложении
Инструмент в этом уроке
Playwright — отдельный урок

Главный принцип Testing Library сформулирован в её документации так: «чем больше ваши тесты похожи на то, как используется ваше ПО, тем больше уверенности они дают». Пользователь не знает про useState, классы CSS и имена пропсов — он видит поле «Phone», кнопку «Send request» и сообщение об ошибке. Тест, который опирается на то же самое, переживает рефакторинг: можно заменить useState на редьюсер или переверстать форму, и тест останется зелёным, пока поведение не изменилось.

Установка: Vitest, jsdom и Testing Library

Vitest — тест-раннер на основе Vite: он читает тот же vite.config.ts, поэтому алиасы, плагины и JSX работают в тестах без отдельной настройки. По документации Vitest 5 требует Vite не ниже 6.4 и Node.js не ниже 22.12. Testing Library для React ставится вместе с @testing-library/dom — это обязательная парная зависимость. jsdom — реализация DOM на JavaScript: по умолчанию Vitest запускает тесты в окружении Node.js, где нет document, и для компонентов окружение переключают на jsdom.

bash
npm create vite@latest app -- --template react-ts
cd app
npm install
npm install -D vitest jsdom @testing-library/react @testing-library/dom @testing-library/user-event @testing-library/jest-dom msw@3
Новый проект React + TypeScript и тестовые зависимости
vite.config.ts
import react from "@vitejs/plugin-react";
import { defineConfig } from "vitest/config";

export default defineConfig({
  plugins: [react()],
  test: {
    environment: "jsdom",
    setupFiles: ["./src/test/setup.ts"],
    unstubGlobals: true,
  },
});
defineConfig из vitest/config добавляет типы для блока test
src/test/setup.ts
import "@testing-library/jest-dom/vitest";
import { cleanup } from "@testing-library/react";
import { afterEach } from "vitest";

afterEach(() => {
  cleanup();
});
Файл подготовки: матчеры jest-dom и очистка DOM после каждого теста
  • environment: "jsdom" — в тестах появляются document, window и DOM-события.
  • setupFiles выполняется перед каждым тестовым файлом. Импорт @testing-library/jest-dom/vitest добавляет к expect матчеры вроде toHaveTextContent, toBeDisabled, toHaveAccessibleDescription.
  • cleanup() размонтирует отрендеренные компоненты. Документация Testing Library говорит, что он вызывается автоматически, только если тест-раннер объявляет глобальный afterEach. В Vitest глобальные функции по умолчанию выключены, поэтому очистку подключаем сами. Мы проверили: без неё два теста формы из этого урока падают, потому что на странице остаются формы из предыдущих тестов и getByRole находит несколько кнопок.
  • unstubGlobals: true — Vitest сам вызывает vi.unstubAllGlobals() перед каждым тестом, и подменённый fetch не протекает в соседние тесты (по умолчанию опция выключена).

Тестовые файлы Vitest находит по имени: в нём должно быть .test. или .spec.. Команда npx vitest запускает тесты в режиме наблюдения и перезапускает их при сохранении файла, npx vitest run — один прогон, как в CI.

Тест чистой функции

Начнём с самого дешёвого и самого надёжного вида теста. Функция cartTotal считает сумму корзины в тенге со скидкой в процентах. Она не трогает DOM и сеть: одни и те же входные данные — один и тот же результат. Такие функции стоит выносить из компонентов именно потому, что их легко тестировать.

src/cart.ts
export type CartItem = { id: string; price: number; qty: number };

export function cartTotal(items: CartItem[], discountPercent = 0): number {
  if (discountPercent < 0 || discountPercent > 100) {
    throw new RangeError("discountPercent must be between 0 and 100");
  }
  const subtotal = items.reduce((sum, item) => sum + item.price * item.qty, 0);
  return Math.round(subtotal * (1 - discountPercent / 100));
}
Расчёт корзины: цена × количество, скидка, округление до тенге
src/cart.test.ts
import { describe, expect, it } from "vitest";
import { cartTotal } from "./cart";

describe("cartTotal", () => {
  it("returns 0 for an empty cart", () => {
    expect(cartTotal([])).toBe(0);
  });

  it("multiplies price by quantity", () => {
    const items = [
      { id: "latte", price: 1800, qty: 2 },
      { id: "croissant", price: 900, qty: 1 },
    ];
    expect(cartTotal(items)).toBe(4500);
  });

  it("applies a percentage discount and rounds to whole tenge", () => {
    expect(cartTotal([{ id: "latte", price: 1800, qty: 2 }], 10)).toBe(3240);
    expect(cartTotal([{ id: "tea", price: 999, qty: 1 }], 15)).toBe(849);
  });

  it("rejects a discount outside 0–100", () => {
    expect(() => cartTotal([], 120)).toThrow(RangeError);
  });
});
describe группирует тесты, it описывает один случай, expect проверяет результат

Каждый it проверяет один случай поведения и называется так, чтобы по отчёту было понятно, что сломалось: пустая корзина, умножение на количество, скидка с округлением, некорректная скидка. Граничные значения — пустой массив, 999 × 0,85 = 849,15 → 849, скидка 120% — находят больше ошибок, чем десять «обычных» корзин.

terminal
$ npx vitest run src/cart.test.ts --reporter=verbose


 RUN  v5.0.3 …/app

 ✓ src/cart.test.ts > cartTotal > returns 0 for an empty cart 1ms
 ✓ src/cart.test.ts > cartTotal > multiplies price by quantity 0ms
 ✓ src/cart.test.ts > cartTotal > applies a percentage discount and rounds to whole tenge 0ms
 ✓ src/cart.test.ts > cartTotal > rejects a discount outside 0–100 0ms

 Test Files  1 passed (1)
      Tests  4 passed (4)
Реальный вывод: 4 теста прошли

Тест, который ни разу не падал, ничего не доказывает. Сломаем функцию намеренно — «забудем» умножить цену на количество:

src/cart.ts
--- a/src/cart.ts
+++ b/src/cart.ts
@@ -7,3 +7,3 @@
   }
-  const subtotal = items.reduce((sum, item) => sum + item.price * item.qty, 0);
+  const subtotal = items.reduce((sum, item) => sum + item.price, 0);
   return Math.round(subtotal * (1 - discountPercent / 100));
Намеренная ошибка для проверки тестов
terminal
$ npx vitest run src/cart.test.ts; echo "exit=$?"


 RUN  v5.0.3 …/app

 ❯ src/cart.test.ts (4 tests | 2 failed) 9ms
   ❯ cartTotal (4)
     × multiplies price by quantity 5ms
     × applies a percentage discount and rounds to whole tenge 1ms

⎯⎯⎯⎯⎯⎯⎯ Failed Tests 2 ⎯⎯⎯⎯⎯⎯⎯

 FAIL  src/cart.test.ts > cartTotal > multiplies price by quantity
AssertionError: expected 2700 to be 4500 // Object.is equality

- Expected
+ Received

- 4500
+ 2700

 ❯ src/cart.test.ts:14:30
     12|       { id: "croissant", price: 900, qty: 1 },
     13|     ];
     14|     expect(cartTotal(items)).toBe(4500);
       |                              ^
     15|   });
     16|


…

⎯⎯⎯⎯⎯⎯⎯ Failed Tests 2 ⎯⎯⎯⎯⎯⎯⎯
 Test Files  1 failed (1)
      Tests  2 failed | 2 passed (4)
exit=1
Реальный вывод (сокращён на месте …): два теста упали, код выхода 1 — CI остановит сборку

Отчёт показывает имя теста, ожидаемое и полученное значение (4500 против 2700) и строку с проверкой. Тест пустой корзины и тест некорректной скидки остались зелёными — они эту ошибку не ловят, и это нормально: каждый тест отвечает за свой случай. После отката правки все четыре теста снова проходят.

Компонент через поведение: роли и user-event

Теперь форма заявки. Она проверяет телефон, отправляет данные функцией sendLead, на время отправки блокирует кнопку и показывает подтверждение или ошибку. Обратите внимание на разметку: подписи связаны с полями через htmlFor, ошибка поля связана через aria-describedby, сообщения имеют роли alert и status. Это нужно и людям со скринридером, и тестам: Testing Library ищет элементы так же, как их находит вспомогательная технология.

src/api.ts
export type Lead = { name: string; phone: string };

export async function sendLead(lead: Lead): Promise<{ id: string }> {
  const res = await fetch("/api/leads", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(lead),
  });
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res.json();
}
Отправка заявки: не-2xx ответ превращается в ошибку
src/LeadForm.tsx
import { useState, type FormEvent } from "react";
import { sendLead } from "./api";

type Status = "idle" | "sending" | "sent" | "error";

const PHONE_RE = /^\+?\d{10,15}$/;

export function LeadForm() {
  const [name, setName] = useState("");
  const [phone, setPhone] = useState("");
  const [phoneError, setPhoneError] = useState("");
  const [status, setStatus] = useState<Status>("idle");

  async function handleSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    if (!PHONE_RE.test(phone.replace(/[\s()-]/g, ""))) {
      setPhoneError("Enter a phone number, e.g. +7 701 123 45 67");
      return;
    }
    setPhoneError("");
    setStatus("sending");
    try {
      await sendLead({ name, phone });
      setStatus("sent");
    } catch {
      setStatus("error");
    }
  }

  if (status === "sent") {
    return <p role="status">Thanks! We will call you back.</p>;
  }

  return (
    <form onSubmit={handleSubmit} noValidate>
      <label htmlFor="lead-name">Name</label>
      <input id="lead-name" value={name} onChange={(e) => setName(e.target.value)} />

      <label htmlFor="lead-phone">Phone</label>
      <input
        id="lead-phone"
        type="tel"
        value={phone}
        onChange={(e) => setPhone(e.target.value)}
        aria-invalid={phoneError ? true : undefined}
        aria-describedby={phoneError ? "lead-phone-error" : undefined}
      />
      {phoneError && (
        <p id="lead-phone-error" role="alert">
          {phoneError}
        </p>
      )}

      {status === "error" && <p role="alert">Could not send. Please try again.</p>}

      <button type="submit" disabled={status === "sending"}>
        {status === "sending" ? "Sending…" : "Send request"}
      </button>
    </form>
  );
}
Форма заявки с валидацией телефона и тремя состояниями

Документация Testing Library задаёт приоритет запросов. Сверху — запросы, доступные всем: getByRole («ваш главный выбор почти для всего»), getByLabelText, getByPlaceholderText, getByText, getByDisplayValue. Ниже — семантические getByAltText и getByTitle. В самом конце — getByTestId: атрибут data-testid не видит и не слышит ни один пользователь, поэтому его оставляют для случаев, когда ни роль, ни текст не подходят.

getBy…

Нет элемента
Бросает ошибку
Когда использовать
Элемент уже должен быть на странице

queryBy…

Нет элемента
Возвращает null
Когда использовать
Проверить, что элемента нет: expect(screen.queryByRole("alert")).toBeNull()

findBy…

Нет элемента
Промис отклоняется после ожидания (по умолчанию 1000 мс)
Когда использовать
Элемент появится позже — после ответа сервера или таймера

Для действий используем user-event, а не fireEvent. fireEvent отправляет одно DOM-событие, а user-event воспроизводит взаимодействие целиком: клик — это наведение, нажатие и отпускание кнопки мыши, фокус; ввод — посимвольные события клавиатуры. Заодно он проверяет, можно ли с элементом взаимодействовать: не кликнет по скрытому элементу и не напечатает в заблокированное поле. Экземпляр создают через userEvent.setup() до рендера, а все методы асинхронные — их нужно ждать через await.

src/LeadForm.test.tsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import { LeadForm } from "./LeadForm";

function setup() {
  const user = userEvent.setup();
  render(<LeadForm />);
  return {
    user,
    name: screen.getByRole("textbox", { name: "Name" }),
    phone: screen.getByRole("textbox", { name: "Phone" }),
    submit: screen.getByRole("button", { name: "Send request" }),
  };
}

describe("LeadForm", () => {
  it("does not send an invalid phone and explains why", async () => {
    const fetchMock = vi.fn();
    vi.stubGlobal("fetch", fetchMock);
    const { user, phone, submit } = setup();

    await user.type(phone, "123");
    await user.click(submit);

    expect(screen.getByRole("alert")).toHaveTextContent("Enter a phone number");
    expect(phone).toHaveAttribute("aria-invalid", "true");
    expect(phone).toHaveAccessibleDescription(/Enter a phone number/);
    expect(fetchMock).not.toHaveBeenCalled();
  });

  it("sends the lead and shows a confirmation", async () => {
    const fetchMock = vi.fn().mockResolvedValue(Response.json({ id: "lead-1" }, { status: 201 }));
    vi.stubGlobal("fetch", fetchMock);
    const { user, name, phone, submit } = setup();

    await user.type(name, "Aigerim");
    await user.type(phone, "+7 701 123 45 67");
    await user.click(submit);

    expect(await screen.findByRole("status")).toHaveTextContent("Thanks!");
    expect(fetchMock).toHaveBeenCalledTimes(1);
    const [url, init] = fetchMock.mock.calls[0];
    expect(url).toBe("/api/leads");
    expect(JSON.parse(init.body)).toEqual({ name: "Aigerim", phone: "+7 701 123 45 67" });
  });

  it("shows an error and lets the user retry when the server fails", async () => {
    vi.stubGlobal("fetch", vi.fn().mockResolvedValue(new Response(null, { status: 500 })));
    const { user, phone, submit } = setup();

    await user.type(phone, "+77011234567");
    await user.click(submit);

    expect(await screen.findByRole("alert")).toHaveTextContent("Could not send");
    expect(screen.getByRole("button", { name: "Send request" })).toBeEnabled();
  });
});
Три сценария формы: ошибка ввода, успех, сбой сервера
  • setup() — обычная функция-помощник: создаёт user, рендерит форму и находит поля по роли и доступному имени. Вызывается внутри каждого теста, как советует документация user-event.
  • Поле type="tel" имеет роль textbox, поэтому оба поля ищутся одинаково: getByRole("textbox", { name: "Phone" }). Если подпись отвяжут от поля, тест упадёт — и это правильная поломка: скринридер тоже перестанет её читать.
  • findByRole("status") ждёт, пока завершится асинхронная отправка и React отрисует подтверждение. Ручные setTimeout и sleep в тестах не нужны.
  • Проверяем то, что видно пользователю (текст, доступность кнопки, aria-invalid), и то, что ушло наружу (адрес и тело запроса). Внутренний status компонента тест не трогает.

Моки сети: vi.fn и MSW

В тесте нельзя ходить в настоящий API: сервер может быть недоступен, а заявка из теста не должна попасть в CRM. Сеть подменяют — делают мок, то есть управляемую заглушку. Вариантов два, и они дополняют друг друга.

`vi.stubGlobal("fetch", vi.fn())`

  • Подменяет глобальный fetch функцией-моком, которая запоминает вызовы и аргументы
  • Ответ задаётся прямо в тесте: mockResolvedValue(Response.json(…))
  • Ничего не нужно ставить — всё есть в Vitest
  • Тест знает, что код использует именно fetch; переход на другой HTTP-клиент сломает моки

MSW (Mock Service Worker)

  • Перехватывает запросы на сетевом уровне: в Node.js — через свой перехватчик, а не подмену fetch
  • Код приложения и тест ничего не знают про мок: работает с fetch, axios и другими клиентами
  • Обработчики можно переиспользовать в разработке и в Storybook
  • Отдельная зависимость и чуть больше настройки

Первый вариант уже есть в LeadForm.test.tsx: vi.fn() создаёт мок, vi.stubGlobal ставит его вместо fetch, а опция unstubGlobals возвращает оригинал перед следующим тестом. Документация Vitest для мокинга запросов прямо рекомендует MSW. Тот же сценарий на MSW выглядит так:

src/LeadForm.msw.test.tsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { http, HttpResponse } from "msw/http";
import { setupServer } from "msw/node";
import { afterAll, afterEach, beforeAll, expect, it } from "vitest";
import { LeadForm } from "./LeadForm";

const received: unknown[] = [];

const server = setupServer(
  http.post("/api/leads", async ({ request }) => {
    received.push(await request.json());
    return HttpResponse.json({ id: "lead-1" }, { status: 201 });
  }),
);

beforeAll(() => server.listen({ onUnhandledFrame: "error" }));
afterEach(() => {
  server.resetHandlers();
  received.length = 0;
});
afterAll(() => server.close());

it("sends the lead over the network", async () => {
  const user = userEvent.setup();
  render(<LeadForm />);

  await user.type(screen.getByRole("textbox", { name: "Phone" }), "+77011234567");
  await user.click(screen.getByRole("button", { name: "Send request" }));

  expect(await screen.findByRole("status")).toHaveTextContent("Thanks!");
  expect(received).toEqual([{ name: "", phone: "+77011234567" }]);
});

it("shows an error when the API is down", async () => {
  server.use(http.post("/api/leads", () => new HttpResponse(null, { status: 503 })));
  const user = userEvent.setup();
  render(<LeadForm />);

  await user.type(screen.getByRole("textbox", { name: "Phone" }), "+77011234567");
  await user.click(screen.getByRole("button", { name: "Send request" }));

  expect(await screen.findByRole("alert")).toHaveTextContent("Could not send");
});
MSW: обработчик POST /api/leads, сброс между тестами и сценарий с ошибкой 503
  • server.listen() включает перехват перед тестами файла, resetHandlers() после каждого теста убирает обработчики, добавленные через server.use(), close() выключает перехват.
  • server.use() на время одного теста подменяет ответ — так проверяется сценарий «API лежит» без отдельного мок-сервера.
  • Обработчик получает настоящий Request: тело читается через request.json(), так что тест видит именно то, что ушло бы в сеть.
terminal
$ npx vitest run src/LeadForm.test.tsx src/LeadForm.msw.test.tsx --reporter=verbose


 RUN  v5.0.3 …/app

 ✓ src/LeadForm.test.tsx > LeadForm > does not send an invalid phone and explains why 252ms
 ✓ src/LeadForm.msw.test.tsx > sends the lead over the network 411ms
 ✓ src/LeadForm.msw.test.tsx > shows an error when the API is down 292ms
 ✓ src/LeadForm.test.tsx > LeadForm > sends the lead and shows a confirmation 500ms
 ✓ src/LeadForm.test.tsx > LeadForm > shows an error and lets the user retry when the server fails 344ms

 Test Files  2 passed (2)
      Tests  5 passed (5)
Реальный вывод: пять тестов формы — три с vi.fn и два с MSW

Что не тестировать и как не делать хрупкие тесты

Хрупкий тест падает, когда поведение не изменилось: переименовали класс, разбили компонент на два, поменяли библиотеку состояния. После нескольких таких падений команда перестаёт доверять тестам и начинает их «чинить» не глядя. Причина почти всегда одна — тест проверяет детали реализации, а не результат.

Хрупко

  • Искать элементы по классам и структуре: container.querySelector(".form > div:nth-child(2) input")
  • Проверять внутреннее состояние, пропсы дочерних компонентов, число рендеров
  • Мокать собственные модули ради «изоляции»: тест проверяет мок, а не код
  • Снапшот всей разметки, который обновляют командой, не читая разницу
  • Ждать фиксированное время вместо появления элемента

Устойчиво

  • Искать по роли и доступному имени: getByRole("button", { name: "Send request" })
  • Проверять видимый результат: текст, доступность кнопки, сообщение об ошибке
  • Мокать только границу с внешним миром: сеть, время, хранилище браузера
  • Точечные проверки того, что важно пользователю
  • Ждать через findBy… и waitFor

Что обычно не стоит тестировать отдельно: сами React и браузер (что onClick вызывается по клику), сторонние библиотеки, внешний вид (цвета и отступы надёжнее ловят визуальные тесты или ревью), тривиальные компоненты без логики. По нашему опыту, больше всего пользы приносят тесты на правила денег и данных (корзина, скидки, нормализация телефона), на формы с валидацией и на ветки ошибок — именно их реже всего проверяют руками.

  • Тест хотя бы раз увидели красным: сломали код и убедились, что тест падает
  • Элементы находятся по роли, подписи или тексту; data-testid — только когда иначе нельзя
  • Действия идут через user-event с await, асинхронный результат ждут через findBy…
  • Сеть подменена; запрос без обработчика роняет тест, а не уходит в настоящий API
  • Моки и подмены сбрасываются между тестами (unstubGlobals, resetHandlers, cleanup)
  • Название теста описывает поведение: по отчёту понятно, что сломалось
  • Тест не меняется, если переписать компонент изнутри без изменения поведения

Упражнения

Следующий шаг — сквозные тесты в настоящем браузере, которые проверяют приложение целиком, и доступность в коде: те же роли и подписи, на которые опираются тесты из этого урока.

Как проверено5 · пройденосверено 11 октября 2026 г.
  • запущено, работает

    vite.config.ts, src/test/setup.ts, cart.ts, cart.test.ts, api.ts, LeadForm.tsx, LeadForm.test.tsx, LeadForm.msw.test.tsx

    временный Vite-проект (npm create vite@latest --template react-ts): tsc -b без ошибок, vitest run — 17/17 тестов, exit 0; Vitest 5.0.3, React 19.3.0, @testing-library/react 16.3.3, user-event 14.6.7, jest-dom 7.0.1, MSW 3.0.3, jsdom 30.1.2, Vite 8.3.4, TypeScript 6.0.3, Node.js 24.18.0

  • запущено, работает

    намеренная ошибка в cartTotal (без * item.qty)

    vitest run src/cart.test.ts: 2 failed | 2 passed, exit 1 — вывод в статье реальный, сокращён; после отката 4/4

  • запущено, работает

    без cleanup в setup.ts

    контрольный прогон: 2 теста LeadForm падают (несколько элементов) — подтверждает утверждение статьи

  • запущено, работает

    MSW onUnhandledFrame: 'error'

    fetch на адрес без обработчика отклоняется; вариант onUnhandledRequest отклонён tsc (TS2353) с MSW 3.0.3

  • запущено, работает

    решения упражнений: exercise-modify.test.tsx, phone.ts + phone.test.ts

    в том же проекте: 1 + 7 тестов проходят; ответ упражнения 1 проверен прогоном без Math.round: 1 failed (expected 849.15 to be 849) | 3 passed

«Запущено» — код выполнен во временном проекте или контейнере, вывод в статье взят из прогона. «Частично» — проверено с заглушками вместо внешних сервисов. «Не запускалось» — нужны устройство, аккаунт или ключ; шаги сверены с официальной документацией. Файлы проверочного проекта и логи запусков сохранены в архиве InterLAB.

Расскажите о задаче — мы изучим запрос и свяжемся с вами

Есть задача?
Обсудим и оценим — бесплатно.

Расскажите о продукте или проблеме. Предложим архитектуру, срок и стоимость первой версии после изучения задачи.

  • Оценка объёма и рисков до старта
  • NDA — по вашему или нашему шаблону
  • Старт работ в течение недели

Не присылайте через форму пароли, ключи доступа и исходный код — безопасную передачу доступа обсудим отдельно.

Нажимая кнопку, вы соглашаетесь с обработкой персональных данных.