493 lines
40 KiB
JavaScript
493 lines
40 KiB
JavaScript
var STORAGE_KEY = "nkt-test-protocol-v1";
|
||
var SHEETJS_SRC = "https://cdn.sheetjs.com/xlsx-0.20.2/package/dist/xlsx.full.min.js";
|
||
|
||
var SECTIONS = [
|
||
{ id: "reg", title: "Регистрация", cases: [
|
||
{ t: "Регистрация (ИП): новый пользователь", pre: "Доступ к тестовой среде сайта НКТ; чистый email, не зарегистрированный в системе",
|
||
steps: ["Перейти на страницу регистрации", "Выбрать тип субъекта ИП; заполнить ИИН представителя организации, ИИН/БИН, наименование организации, имя, фамилию, номер телефона, email, пароль и подтвердить пароль", "Отметить «Согласен с условиями» и пройти проверку «Я не робот»", "Выполнить действие «Отправить код подтверждения»", "Ввести код подтверждения"],
|
||
exp: "Новый пользователь ИП зарегистрирован (подтверждение по почте)" },
|
||
{ t: "Регистрация (ИП): существующий email", pre: "Есть email, который уже зарегистрирован в системе",
|
||
steps: ["Перейти на страницу регистрации", "Выбрать тип субъекта ИП и ввести обязательные данные, уже зарегистрированный email", "Отметить «Согласен с условиями» и пройти проверку «Я не робот»", "Выполнить действие «Отправить код подтверждения»", "Ввести код подтверждения"],
|
||
exp: "Отображается ошибка «Этот email уже зарегистрирован»" },
|
||
{ t: "Регистрация (ИП): повторные ИИН/БИН организации и ИИН представителя", pre: "В системе уже зарегистрированы значения ИИН/БИН организации и ИИН представителя",
|
||
steps: ["Перейти на страницу регистрации", "Выбрать тип субъекта ИП и ввести обязательные данные с ранее зарегистрированными значениями полей ИИН/БИН организации и ИИН представителя организации", "Отметить «Согласен с условиями» и пройти проверку «Я не робот»", "Выполнить действие «Отправить код подтверждения»", "Ввести код подтверждения и выполнить действие «Подтвердить»"],
|
||
exp: "Отображается ошибка «Представитель с таким ИИН уже зарегистрирован в данной организации»" },
|
||
{ t: "Регистрация (ЮЛ): новый пользователь", pre: "Доступ к тестовой среде; чистый email организации",
|
||
steps: ["Перейти на страницу регистрации", "Выбрать тип субъекта ЮЛ; заполнить ИИН представителя организации, ИИН/БИН, наименование организации, имя, фамилию, номер телефона, email, пароль и подтвердить пароль", "Отметить «Согласен с условиями» и пройти проверку «Я не робот»", "Выполнить действие «Отправить код подтверждения»", "Ввести полученный код подтверждения и выполнить действие «Подтвердить»"],
|
||
exp: "Новый пользователь ЮЛ зарегистрирован (подтверждение по почте)" },
|
||
{ t: "Регистрация (ЮЛ): повторные ИИН/БИН организации и ИИН представителя", pre: "В системе уже зарегистрированы значения ИИН/БИН организации и ИИН представителя",
|
||
steps: ["Перейти на страницу регистрации", "Выбрать в поле «Тип субъекта» значение «Юридическое лицо»; заполнить ИИН представителя организации, ИИН/БИН, наименование организации, имя, фамилию, номер телефона, email, пароль и подтвердить пароль", "Отметить «Согласен с условиями» и пройти проверку «Я не робот»", "Выполнить действие «Отправить код подтверждения»", "Ввести полученный код подтверждения и выполнить действие «Подтвердить»"],
|
||
exp: "Ошибка: значения ИИН/БИН организации и ИИН представителя уже зарегистрированы в системе" }
|
||
]},
|
||
{ id: "auth", title: "Авторизация", cases: [
|
||
{ t: "Авторизация: пользователь ИП с ЭЦП", pre: "Зарегистрированный пользователь ИП; его ЭЦП и пароль от неё",
|
||
steps: ["Перейти на страницу авторизации", "Ввести логин и пароль", "Выполнить действие «Войти»", "В окне ЭЦП выбрать свой сертификат", "Ввести пароль от ЭЦП и выполнить действие «Войти» или «Подписать»"],
|
||
exp: "Пользователь авторизован, зашёл в профиль" },
|
||
{ t: "Авторизация (ИП): ЭЦП с другой почтой, чем в системе", pre: "Зарегистрированный пользователь ИП; его сертификат ЭЦП содержит другую почту, чем логин в системе",
|
||
steps: ["Перейти на страницу авторизации", "Ввести логин и пароль", "Выполнить действие «Войти»", "В окне ЭЦП выбрать сертификат с другой почтой", "Ввести пароль от ЭЦП и выполнить действие «Войти» или «Подписать»"],
|
||
exp: "Отображается ошибка «Пользователь сертификата не совпадает с введённым логином. Вход в личный кабинет не выполнен»" },
|
||
{ t: "Авторизация (ИП): ЭЦП принадлежит ЮЛ", pre: "Зарегистрированный пользователь ИП; доступ к ЭЦП, выданной на юридическое лицо",
|
||
steps: ["Перейти на страницу авторизации", "Ввести логин и пароль", "Выполнить действие «Войти»", "В окне ЭЦП выбрать сертификат ЭЦП юрлица", "Ввести пароль от ЭЦП и выполнить действие «Войти» или «Подписать»"],
|
||
exp: "Вход не выполнен" },
|
||
{ t: "Авторизация: пользователь ЮЛ с ЭЦП", pre: "Зарегистрированный пользователь ЮЛ; его сертификат ЭЦП на юрлицо",
|
||
steps: ["Перейти на страницу авторизации", "Ввести логин и пароль", "Выполнить действие «Войти»", "В окне ЭЦП выбрать сертификат ЭЦП юрлица", "Ввести пароль от ЭЦП и выполнить действие «Войти» или «Подписать»"],
|
||
exp: "Пользователь авторизован, зашёл в профиль" },
|
||
{ t: "Авторизация (ЮЛ): ЭЦП принадлежит ИП", pre: "Зарегистрированный пользователь ЮЛ; доступ к сертификату ЭЦП, выданному индивидуальному предпринимателю",
|
||
steps: ["Перейти на страницу авторизации", "Ввести логин и пароль", "Выполнить действие «Войти»", "В окне ЭЦП выбрать сертификат ЭЦП ИП", "Ввести пароль от ЭЦП и выполнить действие «Подписать»"],
|
||
exp: "Вход не выполнен" }
|
||
]},
|
||
{ id: "reset", title: "Восстановление пароля", cases: [
|
||
{ t: "Восстановление пароля", pre: "Регистрированный аккаунт, доступ к email-ящику",
|
||
steps: ["Перейти на страницу авторизации", "Выполнить действие «Забыли пароль?»", "Ввести почту в окне «Восстановление пароля»", "Выполнить действие «Отправить код»", "Получить код сброса на почту, вставить его в окно и нажать «Подтвердить»", "Отобразится окно «Создание нового пароля»: ввести новый пароль и его подтверждение", "Выполнить действие «Сохранить новый пароль»"],
|
||
exp: "Пароль изменён, возврат на страницу авторизации; вход с новым паролем доступен" }
|
||
]},
|
||
{ id: "adm", title: "Кабинет администратора", cases: [
|
||
{ t: "Управление пользователями: роли и блокировки", pre: "Вход как администратор; несколько учётных записей в системе (активные и с правами)", steps: ["Открыть модуль управления пользователями", "Присвоить/сменить роль тестовой учётной записи", "Заблокировать учётную запись", "Разблокировать заблокированную учётную запись", "Выполнить сброс ЭЦП для тестовой учётной записи", "Проверить карточки с информацией об учётных записях"], exp: "Роли меняются, блокировки/разблокировки применяются, сброс ЭЦП выполняется, данные учётных записей отображаются корректно" },
|
||
{ t: "Управление атрибутами: создание, редактирование, удаление", pre: "Вход как администратор; есть доступ к справочнику атрибутов", steps: ["Создать новый атрибут", "Отредактировать созданный атрибут", "Выполнить логическое удаление атрибута", "Проверить, что атрибут скрыт из активного использования, но сохранён в системе"], exp: "Операции выполняются, удалённый атрибут недоступен для выбора, но остаётся в базе" },
|
||
{ t: "Управление составными атрибутами: создание, редактирование, удаление", pre: "Вход как администратор; есть доступ к справочнику составных атрибутов", steps: ["Создать составной атрибут", "Отредактировать составной атрибут", "Выполнить логическое удаление составного атрибута", "Проверить состояние атрибута в справочнике"], exp: "Операции выполняются, удаляется логически, данные не пропадают" },
|
||
{ t: "Привязка расширенных атрибутов к категориям", pre: "Вход как администратор; созданы категории товаров и расширенные атрибуты", steps: ["Выбрать категорию товара", "Привязать к ней расширенные атрибуты", "Сохранить привязку", "Открыть карточку товара в этой категории и проверить атрибуты"], exp: "Привязка сохраняется; атрибуты видны в карточках товаров выбранной категории" }
|
||
]},
|
||
{ id: "user", title: "Кабинет пользователя", cases: [
|
||
{ t: "Профиль: подтверждение ЭЦП", pre: "Вход в кабинет пользователя; сертификат ЭЦП на пользователя", steps: ["Открыть профиль", "Подтвердить ЭЦП"], exp: "ЭЦП подтверждена, статус в профиле обновлён" },
|
||
{ t: "Заявки: просмотр, создание, редактирование, удаление", pre: "Вход в кабинет пользователя", steps: ["Открыть список моих заявок", "Создать новую заявку (через карточку)", "Отредактировать заявку", "Удалить заявку"], exp: "Заявки создаются, редактируются и удаляются; списки обновляются" },
|
||
{ t: "Заявки на редактирование: карточка, модерация, публикация изменений", pre: "Вход в кабинет пользователя; есть действующая карточка товара", steps: ["Создать заявку на редактирование через карточку товара", "Отправить заявку на модерацию", "Учесть результат модерации", "Опубликовать изменения по карточке"], exp: "Изменения проходят модерацию и публикуются в карточке" },
|
||
{ t: "Создание похожего товара на основе действующего", pre: "В кабинете пользователя есть действующий товар", steps: ["Открыть действующую карточку товара", "Выбрать действие «Похожий товар»", "Заполнить отличающиеся поля", "Сохранить заявку"], exp: "Создана заявка с предзаполненными полями исходного товара" },
|
||
{ t: "Мои товары и мои заявки: просмотр", pre: "Опубликовано несколько товаров; созданы заявки", steps: ["Открыть «Мои товары»", "Открыть «Мои заявки»", "Открыть детали позиции"], exp: "Списки корректны, данные совпадают с созданным" },
|
||
{ t: "Массовая загрузка заявок и работа с шаблоном", pre: "Готов шаблон загрузки (Excel) по формату сайта", steps: ["Скачать/открыть шаблон", "Заполнить шаблон данными", "Выполнить массовую загрузку", "Проверить созданные заявки"], exp: "Загрузка принята; заявки и ошибки отображаются корректно" },
|
||
{ t: "Моя номенклатура: вкладки Избранное", pre: "Есть товары, добавленные и не добавленные в избранное", steps: ["Добавить товар в «Моя номенклатура» (вкладка Избранное)", "Снять товар с избранного", "Проверить вкладку Избранное"], exp: "Добавление/снятие работают, вкладка обновляется" }
|
||
]},
|
||
{ id: "mod", title: "Кабинет модератора", cases: [
|
||
{ t: "Модерация заявок: принятие, согласование, отклонение, возврат", pre: "В очереди есть заявки разных статусов", steps: ["Принять заявку в работу", "Согласовать заявку", "Отклонить заявку с комментарием", "Вернуть заявку на доработку", "Проверить статусы в кабинете пользователя"], exp: "Статусы корректно меняются, пользователь видит изменения" },
|
||
{ t: "Пакетная модерация заявок", pre: "В очереди несколько заявок, подлежащих одному решению", steps: ["Выбрать несколько заявок", "Применить пакетное действие (согласовать/отклонить)", "Проверить статусы каждой выбранной заявки"], exp: "Действие применяется ко всем выбранным, статусы обновляются" }
|
||
]},
|
||
{ id: "main", title: "Главный экран", cases: [
|
||
{ t: "Поиск товаров", pre: "Опубликованы товары", steps: ["Ввести поисковый запрос по названию/характеристике", "Открыть результат из выдачи"], exp: "Выдача релевантна запросу, карточки открываются" },
|
||
{ t: "Просмотр информации о проекте", pre: "Главный экран сайта", steps: ["Открыть раздел «О проекте» (ссылка из шапки/подвала)"], exp: "Раздел открывается, информация читается" },
|
||
{ t: "Просмотр карточки товара", pre: "Опубликованы товары с составными атрибутами", steps: ["Открыть карточку товара из списка", "Проверить фото, составные атрибуты, описание"], exp: "Карточка отображается полностью, атрибуты соответствуют привязкам" },
|
||
{ t: "Умный фильтр по товарам", pre: "Созданы атрибуты, привязанные к категориям", steps: ["Открыть умный фильтр", "Выбрать несколько значений атрибутов", "Применить фильтр", "Открыть карточку из отфильтрованного списка"], exp: "Фильтрация работает, комбинация значений даёт корректный список" }
|
||
]}
|
||
];
|
||
|
||
function h(s) {
|
||
s = String(s == null ? "" : s);
|
||
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||
}
|
||
function attr(s) {
|
||
return h(s).replace(/"/g, """);
|
||
}
|
||
function pad2(n) { return String(n).padStart(2, "0"); }
|
||
function todayStr() {
|
||
var d = new Date();
|
||
return pad2(d.getDate()) + "." + pad2(d.getMonth() + 1) + "." + d.getFullYear();
|
||
}
|
||
function fmtDate(iso) {
|
||
if (!iso) return "";
|
||
var p = iso.split("T");
|
||
return p[0].split("-").reverse().join(".");
|
||
}
|
||
|
||
function buildInfoRows() {
|
||
return [
|
||
{ k: "what", l: "Что тестируется", ph: "Название продукта, версия" },
|
||
{ k: "goal", l: "Цель тестирования", ph: "Например: приёмка версии 1.0 сайта НКТ" },
|
||
{ k: "dev", l: "Разработал", ph: "Имя или роль" },
|
||
{ k: "tester", l: "Исполнитель тестов", ph: "Кто выполняет проверки" },
|
||
{ k: "date", l: "Дата проведения", ph: "ДД.ММ.ГГГГ", w: 200 }
|
||
];
|
||
}
|
||
|
||
function infoTableHTML(rows, info) {
|
||
var trs = "";
|
||
rows.forEach(function (r) {
|
||
trs += '<tr><td class="lab"' + (r.w ? ' style="width:' + r.w + 'px"' : "") + ">" + h(r.l) + "</td><td>" +
|
||
'<span class="kt-ai-input" data-k="' + r.k + '" value="' + attr((info || {})[r.k] || "") + '" placeholder="' + h(r.ph) + '"></span></td></tr>';
|
||
});
|
||
return '<table class="doc-table"><tbody>' + trs + "</tbody></table>";
|
||
}
|
||
|
||
function stepsHTML(steps) {
|
||
steps = steps || [""];
|
||
if (!steps.length) steps = [""];
|
||
var s = "";
|
||
steps.forEach(function (st, i) {
|
||
s += '<li><span class="step-num">' + (i + 1) + '</span><span class="step-inner">' +
|
||
'<span class="kt-ai-input" data-kind="step" placeholder="Шаг ' + (i + 1) + '" value="' + attr(st) + '"></span>' +
|
||
'<button class="kt-ai-icon-button step-del" title="Удалить шаг"><svg width="12" height="12" aria-hidden="true"><use href="#x"></use></svg></button></span></li>';
|
||
});
|
||
return s;
|
||
}
|
||
|
||
function caseHTML(c) {
|
||
var st = c.status || "Не выполнен";
|
||
var opts = ["Не выполнен", "Пройден", "Провален", "Заблокирован"].map(function (o) {
|
||
return '<option' + (o === st ? " selected" : "") + ">" + o + "</option>";
|
||
}).join("");
|
||
return '<div class="test-block">' +
|
||
'<div class="test-block-head">' +
|
||
'<span class="num">00</span>' +
|
||
'<span class="kt-ai-input" data-kind="title" placeholder="Название проверки" value="' + attr(c.t) + '"></span>' +
|
||
'<select class="kt-ai-select" data-kind="status" title="Статус">' + opts + '</select>' +
|
||
'<button class="kt-ai-icon-button case-del" title="Удалить кейс"><svg width="14" height="14" aria-hidden="true"><use href="#trash"></use></svg></button>' +
|
||
"</div>" +
|
||
'<table>' +
|
||
'<tr><td class="lab">Предусловия</td><td>' +
|
||
'<span class="kt-ai-input" data-kind="pre" placeholder="Что должно быть готово до проверки" value="' + attr(c.pre) + '"></span></td></tr>' +
|
||
'<tr><td class="lab">Шаги</td><td><div class="steps-cell"><ol class="test-steps">' + stepsHTML(c.steps) +
|
||
'</ol><button class="kt-ai-btn step-add" data-variant="ghost" data-size="sm"><svg width="12" height="12" aria-hidden="true"><use href="#plus"></use></svg> Шаг</button></div></td></tr>' +
|
||
'<tr><td class="lab">Ожидаемый результат</td><td colspan="1">' +
|
||
'<span class="kt-ai-input" data-kind="expected" placeholder="Что должно произойти" value="' + attr(c.exp) + '"></span></td></tr>' +
|
||
'<tr><td class="lab">Фактический результат</td><td>' +
|
||
'<span class="kt-ai-input" data-kind="actual" placeholder="Что случилось на самом деле" value="' + attr(c.act) + '"></span></td></tr>' +
|
||
"</table></div>";
|
||
}
|
||
|
||
function sectionsHTML(state, seed) {
|
||
return SECTIONS.map(function (sec) {
|
||
var key = "sec-" + sec.id;
|
||
var cases = (state && state.sections && state.sections[key]) || seed;
|
||
var hidden = state && state.hidden && state.hidden[key];
|
||
var body = cases.map(caseHTML).join("");
|
||
return '<section class="doc-section" data-id="' + sec.id + '" style="' + (hidden ? "display:none" : "") + '">' +
|
||
'<div class="section-toggle"><button class="kt-ai-icon-button sec-toggle" title="Свернуть/развернуть"><svg width="14" height="14" aria-hidden="true"><use href="#chevronDown"></use></svg></button>' +
|
||
'<h2 class="doc-h2 sec-title">' + h(sec.title) + '</h2><span class="sec-count"></span></div>' +
|
||
'<div class="sec-cases">' + body + '</div></section>';
|
||
}).join("");
|
||
}
|
||
|
||
document.addEventListener("DOMContentLoaded", function () {
|
||
var seed = SECTIONS.map(function (s) { return s.cases; });
|
||
var state = null;
|
||
var raw = null;
|
||
try { raw = localStorage.getItem(STORAGE_KEY); } catch (e) {}
|
||
if (raw) { try { state = JSON.parse(raw); } catch (e) { state = null; } }
|
||
if (!state || state.v !== 2) {
|
||
state = { v: 2, hidden: {}, sections: {}, info: {}, meta: { number: "PT-NKT-001", date: todayStr() }, issues: [{}] };
|
||
}
|
||
if (!state.sections || !Object.keys(state.sections).length) {
|
||
state.sections = {};
|
||
SECTIONS.forEach(function (s, i) { state.sections["sec-" + s.id] = seed[i]; });
|
||
}
|
||
if (!state.info) state.info = {};
|
||
|
||
var banner = document.getElementById("banner-meta");
|
||
var date = state.meta.date || todayStr();
|
||
banner.innerHTML =
|
||
'<span>№ <span class="kt-ai-input" style="width:120px" id="f-number" value="' + attr(state.meta.number || "PT-NKT-001") + '"></span></span>' +
|
||
'<span>от <span class="kt-ai-input" style="width:104px" id="f-date" value="' + attr(state.meta.date || date) + '"></span></span>' +
|
||
'<span>Итог: <span class="kt-ai-chip stat-pill" id="c-pass" data-tone="green" hidden>Прошли: 0</span>' +
|
||
'<span class="kt-ai-chip stat-pill" id="c-fail" data-tone="red" hidden>Провалены: 0</span>' +
|
||
'<span class="kt-ai-chip stat-pill" id="c-block" data-tone="orange" hidden>Заблокировано: 0</span></span>';
|
||
|
||
var info = infoTableHTML(buildInfoRows(), state.info);
|
||
document.getElementById("info-mount").innerHTML = info;
|
||
|
||
var envMount = document.getElementById("env-mount");
|
||
envMount.innerHTML = infoTableHTML([
|
||
{ k: "env", l: "Окружение / адрес", ph: "Например: site.nkt.kz (тестовый стенд)" },
|
||
{ k: "device", l: "Устройство", ph: "ПК, смартфон, планшет" },
|
||
{ k: "browser", l: "Браузер / канал связи", ph: "Chrome 126, 4G, Wi-Fi" },
|
||
{ k: "data", l: "Тестовые данные", ph: "Тестовые аккаунты ИП/ЮЛ, сертификаты ЭЦП" }
|
||
], state.info);
|
||
|
||
document.getElementById("content-mount").innerHTML = sectionsHTML(state, null);
|
||
|
||
var issuesBody = document.querySelector("#issues-body");
|
||
var issues = (state.issues || [{}]).map(function (i, idx) {
|
||
return '<tr><td class="issue-num">' + (idx + 1) + '</td>' +
|
||
'<td><span class="kt-ai-input" data-k="issue-desc" placeholder="Что не работает, как воспроизвести" value="' + attr(i.desc) + '"></span></td>' +
|
||
'<td>' + priSelect(i.pri) + '</td>' +
|
||
'<td>' + statusSelect(i.st) + '</td></tr>';
|
||
}).join("");
|
||
issuesBody.innerHTML = issues;
|
||
|
||
bindAll();
|
||
refresh();
|
||
});
|
||
|
||
function priSelect(v) {
|
||
var opts = ["—", "Критический", "Высокий", "Средний", "Низкий"];
|
||
var s = "";
|
||
opts.forEach(function (o) { s += "<option" + (o === v ? " selected" : "") + ">" + o + "</option>"; });
|
||
return '<select class="kt-ai-select"' + (v ? ' data-val="' + v + '"' : "") + ">" + s + "</select>";
|
||
}
|
||
function statusSelect(v) {
|
||
var opts = ["Открыта", "В работе", "Исправлена"];
|
||
var s = "";
|
||
opts.forEach(function (o) { s += "<option" + (o === v ? " selected" : "") + ">" + o + "</option>"; });
|
||
return '<select class="kt-ai-select"' + (v ? ' data-val="' + v + '"' : "") + ">" + s + "</select>";
|
||
}
|
||
|
||
function refresh() {
|
||
var counts = { p: 0, f: 0, b: 0, n: 0 };
|
||
var total = 0;
|
||
document.querySelectorAll("#content-mount .test-block").forEach(function (b) {
|
||
total += 1;
|
||
var sel = b.querySelector('[data-kind="status"]');
|
||
var st = sel ? sel.value : "Не выполнен";
|
||
if (st === "Пройден") counts.p += 1;
|
||
else if (st === "Провален") counts.f += 1;
|
||
else if (st === "Заблокирован") counts.b += 1;
|
||
else counts.n += 1;
|
||
});
|
||
document.querySelectorAll("#content-mount .doc-section").forEach(function (s) {
|
||
var n = s.querySelectorAll(".test-block").length;
|
||
var pill = s.querySelector(".sec-count");
|
||
if (pill) pill.textContent = n + " кейсов";
|
||
});
|
||
document.querySelectorAll("#content-mount .doc-section").forEach(function (sec) {
|
||
var list = sec.querySelectorAll(".test-block .num");
|
||
list.forEach(function (n, i) { n.textContent = pad2(i + 1); });
|
||
});
|
||
|
||
var setT = function (id, v) { var e = document.getElementById(id); if (e) e.textContent = String(v); };
|
||
setT("s-total", total);
|
||
setT("s-pass", counts.p);
|
||
setT("s-fail", counts.f);
|
||
setT("s-block", counts.b);
|
||
|
||
var cP = document.getElementById("c-pass");
|
||
var cF = document.getElementById("c-fail");
|
||
var cB = document.getElementById("c-block");
|
||
if (cP) { cP.textContent = "Прошли: " + counts.p; cP.hidden = counts.p === 0; }
|
||
if (cF) { cF.textContent = "Провалены: " + counts.f; cF.hidden = counts.f === 0; }
|
||
if (cB) { cB.textContent = "Заблокировано: " + counts.b; cB.hidden = counts.b === 0; }
|
||
|
||
var verdict = document.getElementById("p-verdict");
|
||
var done = total - counts.n;
|
||
var v, tone;
|
||
if (total === 0) { v = "Нет кейсов"; tone = "gray"; }
|
||
else if (done === 0) { v = "Не начат"; tone = "gray"; }
|
||
else if (counts.f > 0) { v = "Есть проблемы"; tone = "red"; }
|
||
else if (counts.b > 0) { v = "Есть блокировки"; tone = "orange"; }
|
||
else if (counts.n > 0) { v = "В процессе"; tone = "blue"; }
|
||
else { v = "Все проверки пройдены"; tone = "green"; }
|
||
verdict.textContent = v;
|
||
verdict.setAttribute("data-tone", tone);
|
||
|
||
var donePct = total ? Math.round((done / total) * 100) : 0;
|
||
document.getElementById("s-done").textContent = done + " / " + total + " (" + donePct + "%)";
|
||
}
|
||
|
||
var saveTimer = null;
|
||
function save() {
|
||
clearTimeout(saveTimer);
|
||
saveTimer = setTimeout(function () {
|
||
try {
|
||
var st = { v: 2 };
|
||
st.meta = { number: document.getElementById("f-number") && document.getElementById("f-number").value, date: document.getElementById("f-date") && document.getElementById("f-date").value };
|
||
st.info = {};
|
||
document.querySelectorAll(".kt-ai-input[data-k]").forEach(function (el) { st.info[el.dataset.k] = el.value; });
|
||
st.sections = {};
|
||
st.hidden = {};
|
||
document.querySelectorAll("#content-mount .doc-section").forEach(function (sec) {
|
||
var key = "sec-" + sec.dataset.id;
|
||
st.hidden[key] = sec.style.display === "none" || sec.classList.contains("collapsed");
|
||
st.sections[key] = Array.prototype.map.call(sec.querySelectorAll(".test-block"), function (b) {
|
||
return {
|
||
t: (b.querySelector('[data-kind="title"]').value || ""),
|
||
status: b.querySelector('[data-kind="status"]').value,
|
||
pre: b.querySelector('[data-kind="pre"]').value || "",
|
||
steps: Array.prototype.map.call(b.querySelectorAll('[data-kind="step"]'), function (s) { return s.value; }),
|
||
exp: b.querySelector('[data-kind="expected"]').value || "",
|
||
act: b.querySelector('[data-kind="actual"]').value || ""
|
||
};
|
||
});
|
||
});
|
||
st.issues = Array.prototype.map.call(document.querySelectorAll("#issues-body tr"), function (tr) {
|
||
return {
|
||
desc: tr.querySelector('[data-k="issue-desc"]').value || "",
|
||
pri: (tr.querySelectorAll("select")[0] || {}).value || "",
|
||
st: (tr.querySelectorAll("select")[1] || {}).value || ""
|
||
};
|
||
});
|
||
localStorage.setItem(STORAGE_KEY, JSON.stringify(st));
|
||
} catch (e) {}
|
||
}, 300);
|
||
}
|
||
|
||
function loadSheetJS() {
|
||
return new Promise(function (res) {
|
||
if (window.XLSX) return res();
|
||
var s = document.createElement("script");
|
||
s.src = SHEETJS_SRC;
|
||
s.onload = function () { res(); };
|
||
s.onerror = function () { window.alert("Не удалось загрузить библиотеку Excel — проверьте интернет"); res(); };
|
||
document.head.appendChild(s);
|
||
});
|
||
}
|
||
|
||
function downloadExcel() {
|
||
loadSheetJS().then(function () {
|
||
if (window.XLSX) downloadExcelInner();
|
||
});
|
||
}
|
||
function downloadExcelInner() {
|
||
if (!window.XLSX) { alert("Не удалось загрузить библиотеку Excel — проверьте интернет"); return; }
|
||
var wb = XLSX.utils.book_new();
|
||
|
||
var infoRows = [["Поле", "Значение"]];
|
||
document.querySelectorAll("#info-mount tr").forEach(function (tr) {
|
||
var td = tr.querySelectorAll("td");
|
||
if (td.length >= 2) infoRows.push([td[0].textContent.trim(), (td[1].querySelector(".kt-ai-input") || {}).value || ""]);
|
||
});
|
||
infoRows.push(["Дата документа", document.getElementById("f-date").value]);
|
||
infoRows.push(["Номер документа", document.getElementById("f-number").value]);
|
||
XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet(infoRows), "Информация");
|
||
|
||
var header = ["№", "Раздел", "Тест-кейс", "Предусловия", "Шаги", "Ожидаемый результат", "Фактический результат", "Статус"];
|
||
var rows = [header];
|
||
var n = 0;
|
||
document.querySelectorAll("#content-mount .doc-section").forEach(function (sec) {
|
||
var title = sec.querySelector(".sec-title").textContent.trim();
|
||
sec.querySelectorAll(".test-block").forEach(function (b) {
|
||
n += 1;
|
||
rows.push([
|
||
n, title,
|
||
b.querySelector('[data-kind="title"]').value || "",
|
||
b.querySelector('[data-kind="pre"]').value || "",
|
||
Array.prototype.map.call(b.querySelectorAll('[data-kind="step"]'), function (s) { return s.value; }).filter(Boolean).join("\n"),
|
||
b.querySelector('[data-kind="expected"]').value || "",
|
||
b.querySelector('[data-kind="actual"]').value || "",
|
||
b.querySelector('[data-kind="status"]').value
|
||
]);
|
||
});
|
||
});
|
||
var ws = XLSX.utils.aoa_to_sheet(rows);
|
||
ws["!cols"] = [{ wch: 4 }, { wch: 22 }, { wch: 38 }, { wch: 40 }, { wch: 70 }, { wch: 42 }, { wch: 42 }, { wch: 14 }];
|
||
XLSX.utils.book_append_sheet(wb, ws, "Тест-кейсы");
|
||
|
||
var irows = [["№", "Описание проблемы", "Приоритет", "Статус"]];
|
||
document.querySelectorAll("#issues-body tr").forEach(function (tr, i) {
|
||
var desc = tr.querySelector('[data-k="issue-desc"]').value;
|
||
if (desc) irows.push([i + 1, desc, (tr.querySelectorAll("select")[0] || {}).value || "", (tr.querySelectorAll("select")[1] || {}).value || ""]);
|
||
});
|
||
if (irows.length === 1) irows.push(["—", "Проблем не обнаружено", "", ""]);
|
||
var iws = XLSX.utils.aoa_to_sheet(irows);
|
||
iws["!cols"] = [{ wch: 4 }, { wch: 70 }, { wch: 14 }, { wch: 14 }];
|
||
XLSX.utils.book_append_sheet(wb, iws, "Проблемы");
|
||
|
||
var name = (document.getElementById("f-number").value || "protocol").replace(/[\\/:*?"<>|]/g, "_");
|
||
XLSX.writeFile(wb, name + ".xlsx");
|
||
}
|
||
|
||
function downloadJSON() {
|
||
var data = {
|
||
meta: { number: document.getElementById("f-number").value, date: document.getElementById("f-date").value },
|
||
info: {}, sections: [], issues: []
|
||
};
|
||
document.querySelectorAll(".kt-ai-input[data-k]").forEach(function (el) { data.info[el.dataset.k] = el.value; });
|
||
document.querySelectorAll("#content-mount .doc-section").forEach(function (sec) {
|
||
var cases = Array.from(sec.querySelectorAll(".test-block")).map(function (b) {
|
||
return {
|
||
title: b.querySelector('[data-kind="title"]').value,
|
||
status: b.querySelector('[data-kind="status"]').value,
|
||
pre: b.querySelector('[data-kind="pre"]').value,
|
||
steps: Array.from(b.querySelectorAll('[data-kind="step"]')).map(function (s) { return s.value; }),
|
||
expected: b.querySelector('[data-kind="expected"]').value,
|
||
actual: b.querySelector('[data-kind="actual"]').value
|
||
};
|
||
});
|
||
data.sections.push({ name: sec.querySelector(".sec-title").textContent.trim(), cases: cases });
|
||
});
|
||
document.querySelectorAll("#issues-body tr").forEach(function (tr) {
|
||
var desc = tr.querySelector('[data-k="issue-desc"]').value;
|
||
if (desc) data.issues.push({ desc: desc, pri: (tr.querySelectorAll("select")[0] || {}).value || "", st: (tr.querySelectorAll("select")[1] || {}).value || "" });
|
||
});
|
||
var blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
|
||
var a = document.createElement("a");
|
||
a.href = URL.createObjectURL(blob);
|
||
a.download = "test-protocol.json";
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
a.remove();
|
||
setTimeout(function () { URL.revokeObjectURL(a.href); }, 5000);
|
||
}
|
||
|
||
function bindAll() {
|
||
document.addEventListener("click", function (e) {
|
||
var t = e.target;
|
||
if (t && t.closest && t.closest("svg")) t = t.closest("button");
|
||
if (!t || !t.classList) return;
|
||
if (t.classList.contains("case-del")) {
|
||
var block = t.closest(".test-block");
|
||
block.remove();
|
||
refresh();
|
||
save();
|
||
} else if (t.classList.contains("step-del")) {
|
||
var li = t.closest("li");
|
||
var ol = li.parentNode;
|
||
if (ol.children.length > 1) {
|
||
li.remove();
|
||
numberSteps(ol);
|
||
save();
|
||
}
|
||
} else if (t.classList.contains("step-add")) {
|
||
var ol2 = t.closest(".steps-cell").querySelector(".test-steps");
|
||
var li2 = document.createElement("li");
|
||
li2.innerHTML = '<span class="step-num"></span><span class="step-inner"><span class="kt-ai-input" data-kind="step" placeholder="Новый шаг"></span><button class="kt-ai-icon-button step-del" title="Удалить шаг"><svg width="12" height="12" aria-hidden="true"><use href="#x"></use></svg></button></span>';
|
||
ol2.appendChild(li2);
|
||
numberSteps(ol2);
|
||
var inp = li2.querySelector(".kt-ai-input");
|
||
if (inp) inp.focus();
|
||
save();
|
||
} else if (t.classList.contains("sec-toggle")) {
|
||
var sec = t.closest(".doc-section");
|
||
sec.classList.toggle("collapsed");
|
||
save();
|
||
} else if (t.classList.contains("issue-add")) {
|
||
document.querySelector("#issues-body").insertAdjacentHTML("beforeend",
|
||
'<tr><td class="issue-num"></td><td><span class="kt-ai-input" data-k="issue-desc" placeholder="Опишите проблему"></span></td><td>' + priSelect("") + '</td><td>' + statusSelect("") + "</td></tr>");
|
||
renumberIssues();
|
||
save();
|
||
} else if (t.classList.contains("issue-del")) {
|
||
var row = t.closest("tr");
|
||
var body = document.querySelector("#issues-body");
|
||
if (body.children.length > 1) {
|
||
row.remove();
|
||
renumberIssues();
|
||
save();
|
||
}
|
||
} else if (t.id === "btn-add") {
|
||
var last = document.querySelector("#content-mount .doc-section:last-child");
|
||
last.insertAdjacentHTML("beforeend", caseHTML({ t: "", status: "Не выполнен", pre: "", steps: [""], exp: "", act: "" }));
|
||
refresh();
|
||
save();
|
||
last.querySelector(".test-block:last-child").scrollIntoView({ behavior: "smooth", block: "center" });
|
||
} else if (t.id === "btn-print") {
|
||
window.print();
|
||
} else if (t.id === "btn-xlsx") {
|
||
downloadExcel();
|
||
} else if (t.id === "btn-json") {
|
||
downloadJSON();
|
||
} else if (t.id === "btn-clear") {
|
||
if (!confirm("Очистить весь протокол и начать заново? Сохранённый черновик тоже будет удалён.")) return;
|
||
try { localStorage.removeItem(STORAGE_KEY); } catch (e) {}
|
||
location.reload();
|
||
}
|
||
});
|
||
|
||
document.addEventListener("input", function (e) {
|
||
var el = e.target;
|
||
if (!el) return;
|
||
var isInfoInput = el.classList && el.classList.contains("kt-ai-input");
|
||
if (!isInfoInput && el.id !== "f-number" && el.id !== "f-date") return;
|
||
if (el.dataset && el.dataset.kind === "step") numberSteps(el.closest(".test-steps"));
|
||
save();
|
||
});
|
||
document.addEventListener("change", function (e) {
|
||
if (e.target.matches(".kt-ai-select[data-kind='status']")) refresh();
|
||
if (e.target.matches("select, input")) save();
|
||
});
|
||
}
|
||
|
||
function numberSteps(ol) {
|
||
if (!ol) return;
|
||
ol.querySelectorAll(".step-num").forEach(function (n, i) { n.textContent = i + 1; });
|
||
}
|
||
|
||
function renumberIssues() {
|
||
document.querySelectorAll("#issues-body tr").forEach(function (tr, i) {
|
||
tr.querySelector(".issue-num").textContent = i + 1;
|
||
});
|
||
}
|