Проверим игру по заранее записанным ожиданиям. Сначала воспроизведём ошибочную раннюю победу, затем добавим небольшой тест правила и исправим ошибку. Отдельно пройдём кнопки, повторные действия и сброс.

Основная задача — проверить свою игру. Для объяснения используется прежний образец «Собери ровно 10», где победа требует ровно трёх разных карт. start содержит финал урока рефакторинга. Правило getOutcome уже в rules.js, поэтому его можно запускать отдельно от кнопок.

Открыть пример отдельно

Ожидаемый результат запишем до запуска

Таблица «нажал и вроде работает» почти не помогает искать ошибку. Для каждого случая нужны исходное состояние, конкретные действия, ожидаемый результат и то, что действительно увидел. Если ожидание записать после запуска, легко принять неверное поведение за правило.

Начало и действияОжидаемЧто записать в наблюдение
Свежая страницаСумма 0, свободны 6 карт, осталось 3 выбораФактические числа и наличие кнопок
1, затем 3, затем 6Победа и блокировка дальнейшего выбораСообщение, сумма, доступность карт
1, затем 2, затем 3Проигрыш с суммой 6Текст и число оставшихся ходов
6, затем 5Перебор 11 завершает партию сразуЗавершилась ли до третьего выбора
4, затем 610 после двух карт ещё не победаОстался 1 выбор; четыре карты доступны
Два быстрых нажатия на одну картуОдна карта, один ходСумма и число оставшихся выборов
Пять сбросов, затем карта 1Сумма 1, осталось 2 выбораНет ли удвоения обработчиков

В комплекте есть пустой worksheets/test-scenarios.md. Заполни его для своей игры и укажи браузер, ширину экрана и способ ввода. Незаполненная клетка не означает PASS. Не объявляй проверенными устройства, на которых не запускал игру.

Проверим правило без интерфейса

Автоматический тест в нашем случае — вызов функции с известными аргументами и сравнение результата с ожиданием. Он полезен тем, что повторяет один и тот же случай после каждой правки. При этом он не знает, удобно ли нажимать кнопки, загрузился ли рисунок и понятно ли сообщение.

Возьми start. Рядом с index.html создай tests.html:

<!doctype html>
<html lang="ru">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>Проверки правила карточной игры</title>
    <link rel="stylesheet" href="style.css">
    <script type="module" src="tests.js"></script>
  </head>
  <body>
    <main>
      <p class="eyebrow">Проверка функции, не всей игры</p>
      <h1>Что решает getOutcome</h1>
      <p id="test-summary" role="status">
        Проверки ещё не выполнены. Если сообщение осталось, проверь Console и загрузку
        tests.js/rules.js.
      </p>
      <div class="test-table">
        <table>
          <thead>
            <tr>
              <th>Случай</th>
              <th>Ожидаем</th>
              <th>Получили</th>
              <th>Проверка</th>
            </tr>
          </thead>
          <tbody id="test-results"></tbody>
        </table>
      </div>
      <p><a href="index.html">Вернуться к игре</a></p>
    </main>
  </body>
</html>

В tests.js импортируй ту же функцию, которую использует игра. Не копируй её реализацию в тест: иначе можно случайно проверить другую версию.

import { getOutcome } from "./rules.js";

const cases = [
  {
    "name": "Свежая партия",
    "total": 0,
    "moves": 0,
    "target": 10,
    "maxMoves": 3,
    "expected": "playing"
  },
  {
    "name": "Ровно10 третьей картой",
    "total": 10,
    "moves": 3,
    "target": 10,
    "maxMoves": 3,
    "expected": "won"
  },
  {
    "name": "Перебор до конца ходов",
    "total": 11,
    "moves": 2,
    "target": 10,
    "maxMoves": 3,
    "expected": "lost"
  },
  {
    "name": "Три карты дали меньше10",
    "total": 6,
    "moves": 3,
    "target": 10,
    "maxMoves": 3,
    "expected": "lost"
  }
];

Массив cases содержит объекты проверок. name описывает случай, числа — вход функции, expected — ожидаемая строка. Эти ожидания получены из правил игры. Если вычислять expected той же функцией getOutcome, проверка всегда сравнивала бы функцию с самой собой.

Ниже массива добавь цикл, который выполнит все проверки:

const results = document.querySelector("#test-results");
const summary = document.querySelector("#test-summary");
let passed = 0;
for (const test of cases) {
  const actual = getOutcome(test.total, test.moves, test.target, test.maxMoves);
  const ok = actual === test.expected;
  if (ok) passed = passed + 1;
  const row = document.createElement("tr");
  const values = [test.name, test.expected, actual, ok ? "PASS" : "FAIL"];
  for (const value of values) {
    const cell = document.createElement("td");
    cell.textContent = value;
    row.appendChild(cell);
  }
  row.className = ok ? "test-pass" : "test-fail";
  results.appendChild(row);
}
summary.textContent = "Прошло " + passed + " из " + cases.length + ".";

actual — то, что вернула функция. Строгое сравнение === не преобразует типы. Для наших строк достаточно именно его; сравнение сложных объектов потребовало бы другого разбора. Каждая строка таблицы создаётся через createElement, а значения записываются textContent. PASS/FAIL показаны текстом, поэтому цвет не является единственным признаком результата.

Оформление таблицы можно взять из step-01/style.css: оно не влияет на проверки. Это первая версия step-01. Открой через HTTP tests.html: должны пройти четыре из четырёх. Открой рядом index.html и пройди ручные сценарии. Четыре зелёные строки ещё не доказывают, что учтены все границы.

Воспроизведём раннюю победу

В папке broken-early-win намеренно изменено правило: оно объявляет победу сразу при total === target. Это учебная ошибка, а не скрытое отличие правильного start. Сначала открой её index.html, выбери 4 и 6. Игра ошибочно объявит победу после двух карт.

Запись ошибки должна позволять повторить её без твоих устных пояснений:

ПолеПример воспроизведения
Версияbroken-early-win; правила требуют 3 карты
НачалоОбновить страницу
ШагиНажать карту 4, затем карту 6
ОжидаетсяПартия продолжается, осталось выбрать 1 карту
ПолученоПобеда после 2 карт
Место поискаgetOutcome в rules.js; слишком ранний return "won"

Открой tests.html этой же папки. Там добавлены граничные случаи, и один из восьми не проходит. Нужный объект проверки:

{
  "name": "4 + 6 — только две карты",
  "total": 10,
  "moves": 2,
  "target": 10,
  "maxMoves": 3,
  "expected": "playing"
}

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

Исправим условие и повторим оба вида проверки

В rules.js версии с ошибкой перенеси победу внутрь проверки числа ходов:

export function getOutcome(total, moves, target, maxMoves) {
  if (total > target) return "lost";
  if (moves === maxMoves) {
    if (total === target) return "won";
    return "lost";
  }
  return "playing";
}

Это step-02, а те же правильные файлы входят в final. В tests.html теперь восемь из восьми. В index.html снова выбери 4 и 6: игра продолжается. Добавь карту 1: сумма 11 завершает партию проигрышем. Сбрось и выбери 1,3,6: это законная победа третьей картой.

Не меняй expected на "won", чтобы красная строка стала зелёной: мы исправляем отклонение от правила. Если само правило сознательно изменилось, сначала зафиксируй это отдельным решением, затем обновляй проверки и интерфейс.

Для своей найденной ошибки заполни worksheets/bug-report.md, сохрани исправление и новый тест в Git. Например:

git diff
git add rules.js tests.html tests.js
git commit -m "Проверяю число карт перед победой"

Проверка функции не заменяет проверку игры

Нажми выбранную карту повторно, быстро нажми две разные, сыграй после пяти сбросов. Затем поставь фокус на карту клавишей Tab и выбери её Enter. Правила должны срабатывать один раз на допустимое действие. Проверь игру на узком экране, чтобы кнопки и сообщение не обрезались.

У этого примера нет таймеров, картинок и сохранения: они не нужны карточной задаче. Поэтому смена вкладки не должна самопроизвольно изменять сумму, а перезагрузка начинает новую партию. В таблице так и запиши; не добавляй системы только ради галочки.

Загрузка rules.js, однако, обязательна. В отдельной копии временно напиши в import "./rule.js". После обновления карточки не создадутся, Network покажет 404, Console сообщит об ошибке модуля. Это воспроизводимая ошибка пути. Верни rules.js и снова проверь и игру, и tests.html. Если страница тестов оставляет «Проверки ещё не выполнены», считать её успешной нельзя.

Если твоя игра использует изображения или сохранение, проверки обязательны уже для неё:

Система в твоей игреКак вызвать проблему в тестовой копииЧто должно быть проверено
ИзображениеВременно указать неверный путьЗапасное изображение или сообщение об ошибке; игру можно продолжить либо понятно, почему она не запускается
localStorageВ своём тестовом ключе записать испорченный JSONНет падения; безопасное начальное состояние и понятное сообщение
Запись настройкиПроверить отказ хранилища доступным способомВыбор текущей сессии работает; ошибка не скрыта
Таймер или RAFНесколько стартов/сбросов и уход со вкладкиНет второго цикла, лишнего списания времени или старого ввода

Для сохранений вспомни обработку из урока 20, для ресурсов — загрузку из урока 29. Порти только данные своей тестовой копии и свой ключ. Если конкретный отказ удалось лишь смоделировать, запиши это отдельно от реальной настройки браузера. Шаблон позволяет отметить «не применяется» с причиной.

Самостоятельно: добавь проверки и исправь ошибку

Добавь две проверки для возможного варианта задачи: цель 14, ровно 4 карты. Одна должна отличать достигнутую сумму до четвёртой карты от победы, другая — подтвердить победу на четвёртой. Пока меняется только набор тестовых данных, основную демонстрацию с целью 10 менять не нужно.

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

Подсказка 1: сначала числа и ожидание

Запиши пары total/moves для цели 14: одинаковая сумма, но разное число использованных карт. Expected должен следовать формулировке «ровно 4», а не результату текущего запуска.

Подсказка 2: тест не должен чинить функцию

Добавь объекты в cases, не меняя getOutcome. Если проверка не проходит, сначала сравни правило, входы и возвращаемую строку. Для собственной функции выбери аналогичную границу.

Разбор после попытки

В solutions/extra-cases total14/moves3 даёт playing, а total14/moves4 — won при target14/maxMoves4. Всего проходят десять проверок. Решение показывает формат, но случаи и ошибка твоей игры должны быть собственными.

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

Нужны заполненная таблица с фактическими наблюдениями, повторяемые шаги хотя бы одной ошибки, исправление и повторная проверка. Отдельно отметь непроверенное. Тесты проверяют правило. Отдельно нужно проверить внешний вид, быстрые действия, перезапуск и загрузку файлов. Если ты можешь объяснить, почему каждый expected такой, проверки помогают понимать код, а не только получать зелёный экран.

Справочно: строгое сравнение и создание элементов на MDN. Для этого набора проверок отдельный тестовый пакет не нужен.