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

А вот пользовательская документация живёт ��уда более сложной жизнью. Её либо не пишут вовсе, надеясь на хороший UX, либо пишут один раз и больше к ней не возвращаются. Через несколько месяцев оказывается, что интерфейс уже другой, кнопки переехали, а скриншоты выглядят как привет из прошлого релиза.

В этой статье я хочу показать подход, при котором автоматизированные e2e-тесты используются не только для проверки работоспособности продукта, но и для автоматического создания актуального пользовательского руководства — на базе реальных сценариев и реального интерфейса.

Где вообще место пользовательской документации

Если разложить разработку продукта на этапы, получится примерно такая схема (упрощённая, но показательная):

  1. Салфеточный прототип — начальная идея об продукте, которая просто в общих чертах описывает то, что мы придумали и хотим попробовать реализовать

  2. Аналитика и Бизнес-документация — здесь мы уже проанализировали идею и подробно описали, как вообще эта штука работает; бывает, что на этом пункте процесс заканчивается, т.к. оно не рентабельно / не интересно / нерешаемо

  3. Дизайн — в этот момент к нам приходят магистры и феи Фигмы, реализовав в интерфейсах всё, что было придумано ранее

  4. Разработка — место, где ожидания превращаются в реальность, или не превращаются (тут как повезёт)

  5. Тестирование — здесь мы сравниваем то, что было запланировано с тем, что получилось, и главное, чтобы всё ещё нормально работало

  6. Релиз — отправляем этот корабль в плавание

  7. Об��луживание — следим за кораблём, добавляем новые возможности и чиним при необходимости

Первые этапы сразу отпадают: на них просто нечего показывать пользователю. Дизайн — вариант получше, но итоговый интерфейс частенько не совпадает с макетами на 100%. И тут даже дело не в том, что на этапе разработки ожидание и реальность так и не сошлись. Бывает прощё — в дизайне на какой-то условной карточке было название из двух слов, а описание из десяти. А по факту пользователи название меньше, чем из 10 слов, даже не считают достойным быть — и по-другому никак.

Релиз — это вообще отдельное таинство доставки кода до пользователей.

Пост-релизное обслуживание будто бы подходит — продукт уже выпущен и можно даже написать это самое руководство, как это обычно и делается. Да, это требует времени, но актуализация документации тоже важная задача.

А вот тестирование я упустил нарочно, ибо по правилам повествования оно должно быть именно здесь, чтобы не разрывать ход мысли 😎. В хороших командах новые функции всё равно проходят через e2e-сценарии, которые повторяют реальные действия пользователя. И если мы уже открываем страницы, нажимаем кнопки, заполняем формы и проверяем результат, то почему бы не использовать этот процесс ещё и для создания пользовательского руководства?

Идея подхода

Ключевая мысль простая:

Один пользовательский сценарий = один тест + один фрагмент документации

Тест проверяет, что функциональность работает, а параллельно:

  1. фиксирует шаги

  2. делает скриншоты интерфейса

  3. формирует читаемое описание действий пользователя.

Таким образом, документация всегда соответствует текущему состоянию продукта — ровно потому, что она создаётся тем же кодом, что и тесты.

Практика: небольшой MVP

Чтобы не витать в абстракциях, я взял своё небольшое приложение — Tasker.app, простой менеджер задач без базы данных.

В качестве инструмента для автоматизации использую Puppeteer. Это не принципиально — подойдёт любая библиотека для e2e-тестирования, но с Puppeteer я уже работал, поэтому выбор пал на него.

В итоге я хочу получить:

  1. проверку реальных пользовательских сценариев

  2. отдельную «статью» на каждый сценарий

  3. набор статичных HTML-страниц с изображениями, которые легко опубликовать или доработать.

Пусть наша система называется FlowDocs.

Ядро системы

Начнём с главного — установки самой библиотеки:

npm i puppeteer

И заодно сразу создадим файл index.js, который будет корневым для нашего приложения. Пока оставим его пустым, лишь сразу, чтобы не возвращаться, добавим в package.json команду для старта:

"scripts": {
  "start": "node index.js"
}

Так как сценариев будет много, всю работу с браузером выносим в отдельный модуль. Например, функция клика по элементу:

/**
 * Нажатие на элемент по ID
 */
async function clickById(page, elementId, timeout = 5000) {
  try {
    await page.waitForSelector(`#${elementId}`, { timeout });
    await page.click(`#${elementId}`);
    return true;
  } catch (error) {
    console.warn(`clickById: элемент #${elementId} не найден`, error.message);
    return false;
  }
}

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

Генерация документации

Для формирования итогового руководства используется отдельный класс Reporter. Он отвечает за:

  • заголовки

  • текст

  • изображения

  • ссылки

  • генерацию HTML

Упрощённая структура выглядит так:

class Reporter {
  addHeader(text, level = 2) {}
  addParagraph(text) {}
  addLink(url, text) {}
  addImage(image, description = '') {}
  generateHTML(reportName = 'test-report') {}
  async save(reportName) {}
  clear() {}
}

Для MVP этого достаточно, а при необходимости функциональность легко расширяется.

Экшены и повторяющиеся паттерны

Теперь поговорим про конкретное тестируемое приложение и подумаем — если ли у нас какие-то повторяющиеся паттерны в действиях, которые мы хотим так же вынести в одну функцию? Если да, то логично добавить экшены, которые будут комбинировать стандартные функции. Например, модальные окна:

async function getModalWindow(page, className, timeout = 5000) {
  try {
    await page.waitForSelector(`.${className}`, { timeout });

    const result = await page.evaluate((selector) => {
      const element = document.querySelector(selector);
      if (!element) return null;

      const parent = element.parentElement;
      const { x, y, width, height } = element.getBoundingClientRect();

      return {
        id: parent.id,
        position: { x, y, width, height }
      };
    }, `.${className}`);

    return {
      ...result,
      async setInput(input, text) {
        await fillFieldById(page, `${result.id}-${input}`, text);
      }
    };
  } catch {
    return null;
  }
}

В результате сценарии становятся компактнее и читаемее.

Пользовательские сценарии

Каждый сценарий — это:

  1. тест

  2. будущая глава пользовательского руководства

Пример фрагмента сценария создания задачи:

reporter.addHeader('Создание новой задачи', 1);
reporter.addParagraph('Шаг 1: Открываем главную страницу приложения');

await loadPage(page, 'https://anatolykulikov.ru/app/tasker/');
await takeScreenshot(page, 'initial-page.png', { dir: reportName });
reporter.addImage('initial-page.png');

Или работа с модальным окном:

const modalCreate = await getModalWindow(page, 'dialog');
const { position } = modalCreate;

reporter.addParagraph('Шаг 3: Откроется окно добавления задачи');
await takeScreenshot(page, 'modal-window.png', {
  clip: createClipWithPadding(position),
  dir: reportName
});
reporter.addImage('modal-window.png');

В результате сценарий одновременно проверяет функциональность, фиксирует интерфейс и формирует понятную пошаговую инструкцию.

Финальная сборка

В самом начале мы оста��или одиноким index.js — пришло время наполнить и его. Поскольку это наша центральная точка, то здесь мы будем формировать главную страницу руководства, а также запускать те сценарии, которые нам нужны.

Запускаем процесс самой простой командой npm start (или можно node index.js, в нашем случае это одно и то же) и просто наблюдаем за процессом.

Итоги

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

Описанный подход — это, по сути, MVP. Его можно развивать дальше: подключать CI/CD, автоматически публиковать документацию после релизов, добавлять версионирование или локализацию. Всё это ложится на уже существующую инфраструктуру тестирования и не требует отдельного процесса.

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