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

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

На примере Modus BI разберём пять багов, которые нашли с помощью ручного тестирования. Это не история про «кликнуть по кнопке и сверить текст». Каждый кейс показывает конкретный тип логической проблемы: неверный выбор источника данных для отображения, разрыв передачи состояния, ошибка на границе выборки, проблема рендера при длинном контенте и отсутствие сетевого запроса после действия пользователя.

Что важно понимать перед кейсами

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

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

SELECT * FROM table;

Но интерфейс не всегда должен показывать пользователю физические имена полей из базы. В БД поле может называться client_id, created_at, order_sum или region_code. Для разработчика это нормальные технические имена. Для пользователя отчёта чаще нужны понятные подписи: «Клиент», «Дата создания», «Сумма заказа», «Регион».

Значит, у одного поля появляется несколько представлений: физическое имя в базе, имя из набора данных и пользовательский заголовок. Компонент интерфейса должен выбрать правильное значение по приоритету, а не просто взять первое доступное поле из metadata.

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

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

Кейс 1. Модальное окно берёт не тот источник имени поля

Первый баг выглядел почти незаметно. Модальное окно открывалось, данные не пропадали, ошибок на экране не было. Но компонент показывал не то значение, которое ожидал пользователь.

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

Здесь важно не смешивать три уровня:

  1. Физическое имя в базе данных. Техническое имя, с которым работает хранилище.

  2. Имя поля в наборе данных. Значение, которое система получила из источника.

  3. Пользовательский заголовок. Название, которое задано в интерфейсе для отображения.

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

Что должно было работать

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

Логика выглядит так:

если задан пользовательский заголовок → показываем его

иначе → показываем наименование поля из набора данных

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

Что было замечено

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

В настройках таблицы всё выглядело корректно: пользователь ввёл название, интерфейс его принял, визуально настройка сохранилась. Баг проявился дальше. После открытия модального окна система показала не заданный заголовок, а исходное наименование поля из набора данных.

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

Почему это важно для разработки

Этот баг показывает проблему не в тексте на экране, а в выборе источника значения. В интерфейсе должно быть единое правило: компоненту не нужно каждый раз самостоятельно решать, что показывать. Лучше, когда есть общая функция, которая возвращает уже рассчитанное display-значение.

Условный приоритет может выглядеть так:

→ пользовательский заголовок

→ отображаемое имя из набора данных

→ физическое имя поля

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

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

Как это было найдено и исправлено

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

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

Вывод

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

Кейс 2. Date picker работает, но состояние не доходит до таблицы

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

Именно здесь появился баг. Компонент фильтра выглядел исправным, но таблица, которая зависела от выбранного диапазона, не обновляла данные.

Что должно было работать

В системе Modus BI был фильтр по диапазону дат. Он состоял из двух полей: «От» и «До». Пользователь нажимал на иконку календаря, выбирал год, месяц и конкретную дату. Такой сценарий важен, когда ручной ввод заблокирован и дату можно задать только через календарь.

После выбора даты должна сработать цепочка:

→ пользователь выбирает дату

→ date picker обновляет значение

→ фильтр обновляет состояние

→ формируются параметры фильтрации

→ связанная таблица получает новые параметры

→ данные в таблице обновляются

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

Что было замечено

Проверка шла в два этапа.

Сначала тестировался сам контейнер фильтра. В этом сценарии всё работало: календарь открывался, год, месяц и день выбирались, при блокировке ручного ввода поле редактировалось только через календарь. Атомарная проверка компонента прошла успешно.

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

На этом этапе появилась проблема. Фильтр продолжал выглядеть корректно, но таблица не фильтровала данные согласно выбранным значениям. Пользователь задавал диапазон, интерфейс принимал выбор, но связанный компонент оставался в прежнем состоянии.

Почему это важно для разработки

Date picker может корректно менять значение внутри себя, но для интерфейса этого недостаточно. Нужно проверить, дошло ли изменение до слоя, который отвечает за данные таблицы.

С инженерной стороны здесь важна не визуальная работа календаря, а передача состояния. Нужно понять, что происходит после выбора даты:

→ значение date picker

→ состояние фильтра

→ объект параметров

→ событие изменения

→ связанный компонент

→ запрос или пересчёт данных

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

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

Как это было найдено

Баг нашли именно потому, что проверка не остановилась на одном компоненте. Сначала протестировали сам фильтр: внешний вид, выбор даты, блокировку ручного ввода. Затем добавили связанный сценарий с таблицей.

Если бы проверка ограничилась date picker, баг остался бы незамеченным. Формально доработка фильтра прошла бы тест, но пользовательский сценарий всё равно был бы сломан.

Вывод

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

Для таких сценариев важно проверять не только контрол, но и весь путь состояния: от действия пользователя до обновления данных.

Кейс 3. Таблица теряет строку на границе выборки

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

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

Что должно было работать

В контейнере «Таблица» есть настройка «Лимит при показе (строки)». Она задаёт максимальное количество строк, которое пользователь видит на экране, даже если в источнике данных записей больше. Например, если в источнике 1000 записей, а лимит установлен на 10, таблица должна показать 10 строк. Данные в источнике при этом не исчезают.

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

Схема выглядит так:

→ данные таблицы

→ расчёт общих итогов по всему набору

→ применение лимита к строкам на экране

→ вывод ограниченного списка строк и общих итогов

Лимит отвечает за отображение. Итоги отвечают за расчёт. Если смешать эти задачи, пользователь получит неверный результат.

Что было замечено

Сначала был создан дашборд с двумя контейнерами: фильтром и таблицей. В таблице включили «Лимит при показе (строки)» и «Итоги».

При проверке этой связки всё работало согласно требованиям. Независимо от количества строк на экране таблица показывала общие итоги по всем данным. Основная доработка выглядела корректной.

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

И здесь появился баг. После добавления фильтра и применения фильтрации таблица отображала на одну строку меньше, чем ожидалось. Например, нужно было вывести 4 записи, а на экране появлялись только 3.

Таблица не падала. Фильтр применялся. Данные были на месте. Но количество строк не совпадало с ожидаемым результатом. Это неприятный тип ошибки: строка не выглядит сломанной, она просто не появляется.

Почему это важно для разработки

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

В разработке похожие случаи часто относят к классу off-by-one, то есть ошибкам «на единицу». Они возникают, когда код неверно считает границы диапазона: включает или исключает последнюю запись, начинает отсчёт не с того индекса или неправильно сравнивает количество элементов с лимитом.

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

Например:

показать строки с 0 по 3 включительно → 4 строки

показать строки с 0 по 3 не включительно → 3 строки

Для пользователя разница простая: он ждёт 4 записи, а получает 3. Для разработчика это повод проверить порядок применения фильтра, лимита и расчёта итогов. Особенно если часть операций выполняется на клиенте, часть на сервере, а интерфейс отдельно считает количество строк для отображения.

Почему проверка только основной доработки не спасает

В исходном требовании акцент был на связке «лимит строк + итоги». Эта часть работала корректно. Но баг нашёлся в соседнем сценарии: лимит строк + фильтр.

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

Фильтр здесь добавили не случайно. Это типовой пользовательский сценарий: в больших таблицах пользователь ограничивает вывод и фильтрует данные, чтобы быстрее найти нужные записи.

Как это было найдено

Баг нашли во время отдельной проверки лимита и его поведения в связке с фильтром. Сначала проверили основную доработку: лимит вместе с итогами. Затем стали тестировать настройки независимо друг от друга.

После добавления фильтра стало видно, что таблица не выводит последнюю ожидаемую строку. Если должно было появиться 4 записи, фактически отображались 3.

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

Вывод

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

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

Кейс 4. Tooltip появляется, но ломает читаемость слоя

Четвёртый кейс связан с рендером. Данные не пропали, диаграмма построилась, всплывающая подсказка появилась. Но интерфейс всё равно работал некорректно: подсказка перекрыла легенду, и пользователь не мог прочитать ни одно, ни другое.

Это не ошибка расчёта. Это ошибка поведения визуального компонента при нестандартном содержимом.

Что должно было работать

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

Tooltip работает как дополнительный слой. Он не должен ломать читаемость основного графика и соседних элементов. Сценарий выглядит так:

→ пользователь наводит курсор на элемент диаграммы

→ система показывает подсказку

→ пользователь читает содержимое подсказки

→ легенда остаётся читаемой

С точки зрения реализации компонент должен рассчитать размер подсказки, положение, перенос строк, фон, отступы и поведение рядом с легендой.

Что было замечено

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

При наведении курсора подсказка отображалась. Событие наведения обработалось, блок появился, данные попали в интерфейс. Но подсказка наложилась на легенду диаграммы. В результате часть информации стала нечитаемой: пользователь не мог нормально прочитать ни подсказку, ни значения в легенде.

То есть компонент формально выполнил действие, но не учёл контекст, в котором отрисовывается.

Почему это важно для разработки

Такие дефекты лежат в зоне layout-логики: позиционирования, расчёта размеров, слоёв и поведения при длинном содержимом. Tooltip не может рассчитываться только на короткие строки и свободное место рядом.

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

С инженерной точки зрения здесь нужно проверить:

  1. как компонент определяет ширину и высоту подсказки;

  2. переносит ли длинный текст;

  3. учитывает ли границы области диаграммы;

  4. не перекрывает ли легенду;

  5. есть ли у подсказки непрозрачный фон;

  6. корректно ли работает слой отображения поверх диаграммы.

Если подсказка появляется поверх легенды без корректного фона или позиции, пользователь видит наложение текста на текст. Данные есть, но работать с ними нельзя.

Как это было найдено и исправлено

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

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

Вывод

Этот кейс показывает, что отрисовать данные недостаточно. Нужно проверить, сможет ли пользователь их прочитать.

Для тестировщика здесь важен простой принцип: интерфейс нужно проверять на неудобных данных. Длинные строки, большие значения и плотные визуальные блоки быстро показывают слабые места рендера.

Кейс 5. Кнопка нажимается, но запрос не уходит

Пятый кейс Modus BI связан с импортом Excel. Интерфейс внешне почти не давал признаков поломки. Пользователь выбирал файл, видел его содержимое, заполнял нужные поля и нажимал кнопку импорта. Ошибки на экране не было. Форма не падала. Предпросмотр работал. Но ключевое действие не выполнялось: файл не импортировался.

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

Что должно было работать

В разделе загрузки данных пользователь выбирает файл .xlsx, система показывает содержимое, затем пользователь задаёт имя для отображения после импорта и нажимает «Импортировать данные».

После этого должна запуститься цепочка:

→ выбор файла

→ чтение и предпросмотр содержимого

→ заполнение параметров импорта

→ нажатие кнопки

→ отправка запроса

→ обработка файла на сервере

→ создание таблицы в выбранной схеме БД

→ создание набора данных в системе

→ сообщение об успешной загрузке

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

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

Что было замечено

Был выбран файл .xlsx. Система отобразила его содержимое. Затем было заполнено имя файла для отображения внутри системы. После нажатия на кнопку «Импортировать данные» ожидалось сообщение об успешной загрузке, создание таблицы в выбранной схеме БД и формирование набора данных.

Но этого не произошло. Файл не импортировался. Таблица в БД не создавалась. Набор данных в системе не появлялся.

Сначала была проверена гипотеза о некорректных данных. Иногда импорт может ломаться из-за спецсимволов, неподдерживаемого формата, пустых колонок или неожиданных типов. Но данные были обычными. При выборе другого файла поведение повторилось. Ошибок интерфейс не показывал.

Значит, нужно было смотреть не на содержимое Excel, а на то, что происходит после клика.

Почему это важно для разработки

Ключевой сигнал — отсутствие сетевого запроса. Через DevTools стало видно, что при нажатии на кнопку «Импортировать данные» запрос не отправляется.

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

С инженерной точки зрения проблема находится до API:

→ клик по кнопке

→ обработчик события

→ проверка состояния формы

→ подготовка данных для запроса

→ отправка запроса

Сам факт отсутствия запроса не говорит, где именно ошибка в коде. Но он показывает направление диагностики: обработчик клика, состояние формы, фронтенд-валидация, сбор параметров или логика отправки.

Как это было найдено

Баг нашли через DevTools. Сначала проверили пользовательские версии: корректность файла, отсутствие спецсимволов, повтор на другом .xlsx. После этого открыли панель сетевых запросов и повторили сценарий.

В нормальном случае после клика должен появиться запрос к серверу. Но запроса не было.

Это отделило ошибку интерфейса от ошибки обработки данных. Если бы сервер вернул ошибку, её можно было бы смотреть по статусу ответа, телу ответа или сообщению в интерфейсе. Здесь система даже не начала операцию импорта.

Вывод

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

Когда интерфейс молчит, DevTools помогает понять, где остановилась цепочка: до запроса, на запросе или после ответа сервера.

Что объединяет эти кейсы

Эти баги относятся к разным частям интерфейса, но у них общий механизм: ошибка появилась не в очевидной точке, а на переходе между действием пользователя и программной цепочкой за ним.

Модальное окно открылось, но взяло не тот источник имени поля. Date picker работал, но значение не дошло до таблицы. Таблица обновилась, но потеряла строку на границе выборки. Tooltip появился, но сломал читаемость легенды. Кнопка импорта нажалась, но запрос не ушёл.

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

Поэтому проверка отдельного компонента даёт только часть картины. Нужно смотреть, что происходит после действия:

  1. какое состояние изменилось;

  2. какие параметры сформировались;

  3. какой компонент должен получить обновление;

  4. ушёл ли запрос;

  5. какие данные вернулись;

  6. как компонент отрисовал результат.

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

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

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

Заключение

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

Для тестировщика важна не только внимательность, но и понимание внутренней логики продукта. Нужно видеть, что происходит за интерфейсом: откуда приходит значение, какой компонент его использует, куда должен уйти запрос, какие данные должны обновиться и какой результат пользователь должен получить.

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

P.S. Присоединяйтесь к нашему BI-сообществу в Telegram и будьте в курсе последних новостей Modus!