1871 lines
58 KiB
JavaScript
1871 lines
58 KiB
JavaScript
(function () {
|
|
"use strict";
|
|
|
|
var config = window.ObyteQaTool || {};
|
|
|
|
function onReady(callback) {
|
|
if (document.readyState === "loading") {
|
|
document.addEventListener("DOMContentLoaded", callback);
|
|
} else {
|
|
callback();
|
|
}
|
|
}
|
|
|
|
onReady(function () {
|
|
document.querySelectorAll("[data-obyte-qa-tool]").forEach(function (root) {
|
|
if (root.dataset.oqtReady === "1") {
|
|
return;
|
|
}
|
|
root.dataset.oqtReady = "1";
|
|
new QaTool(root);
|
|
});
|
|
});
|
|
|
|
function QaTool(root) {
|
|
this.root = root;
|
|
this.template = null;
|
|
this.dragIndex = -1;
|
|
this.gitlabTemplatePath = "";
|
|
this.gitlabTemplateModule = "";
|
|
this.storageKey = "obyteQaToolState:" + window.location.pathname;
|
|
this.els = this.collectElements();
|
|
this.init();
|
|
}
|
|
|
|
QaTool.prototype.collectElements = function () {
|
|
var root = this.root;
|
|
var field = function (name) {
|
|
return root.querySelector('[data-field="' + name + '"]');
|
|
};
|
|
var action = function (name) {
|
|
return root.querySelector('[data-action="' + name + '"]');
|
|
};
|
|
|
|
return {
|
|
tplName: field("tplName"),
|
|
stepSummary: field("stepSummary"),
|
|
statusTag: field("statusTag"),
|
|
gitlabTplSelect: field("gitlabTplSelect"),
|
|
gitlabTplStatus: field("gitlabTplStatus"),
|
|
docbeeTokenStatus: field("docbeeTokenStatus"),
|
|
templateFile: field("templateFile"),
|
|
module: field("module"),
|
|
moduleVersion: field("moduleVersion"),
|
|
olmNummer: field("olmNummer"),
|
|
pbxVersion: field("pbxVersion"),
|
|
tester: field("tester"),
|
|
docbeeUrl: field("docbeeUrl"),
|
|
stepsTable: field("stepsTable"),
|
|
stepsTableBody: field("stepsTableBody"),
|
|
loadJson: field("loadJson"),
|
|
message: field("message"),
|
|
reloadGitlab: action("reloadGitlab"),
|
|
pickTemplate: action("pickTemplate"),
|
|
addStep: action("addStep"),
|
|
addGroup: action("addGroup"),
|
|
saveJson: action("saveJson"),
|
|
pickRun: action("pickRun"),
|
|
exportAll: action("exportAll"),
|
|
exportMd: action("exportMd"),
|
|
exportCsv: action("exportCsv"),
|
|
printPdf: action("printPdf"),
|
|
exportTemplateYaml: action("exportTemplateYaml"),
|
|
pushDocbee: action("pushDocbee")
|
|
};
|
|
};
|
|
|
|
QaTool.prototype.init = function () {
|
|
var self = this;
|
|
|
|
if (this.els.tester) {
|
|
this.els.tester.value = config.tester || "";
|
|
}
|
|
if (this.els.pbxVersion && config.defaultPbxVersion) {
|
|
this.els.pbxVersion.value = config.defaultPbxVersion;
|
|
}
|
|
|
|
this.restoreAutosave();
|
|
this.bindMetaAutosave();
|
|
this.bindControls();
|
|
this.updateServiceBadges();
|
|
this.updateRunSummary();
|
|
|
|
if (config.gitlabEnabled) {
|
|
this.populateGitlabDropdown();
|
|
}
|
|
};
|
|
|
|
QaTool.prototype.bindMetaAutosave = function () {
|
|
var self = this;
|
|
["input", "change"].forEach(function (eventName) {
|
|
[self.els.module, self.els.moduleVersion, self.els.olmNummer, self.els.pbxVersion, self.els.docbeeUrl].forEach(function (el) {
|
|
if (el) {
|
|
el.addEventListener(eventName, function () {
|
|
self.updateAutosave();
|
|
});
|
|
}
|
|
});
|
|
});
|
|
};
|
|
|
|
QaTool.prototype.bindControls = function () {
|
|
var self = this;
|
|
|
|
bind(this.els.reloadGitlab, "click", function (event) {
|
|
event.preventDefault();
|
|
self.populateGitlabDropdown();
|
|
});
|
|
|
|
bind(this.els.pickTemplate, "click", function () {
|
|
if (self.els.templateFile) {
|
|
self.els.templateFile.click();
|
|
}
|
|
});
|
|
|
|
bind(this.els.templateFile, "change", function (event) {
|
|
self.loadLocalTemplate(event);
|
|
});
|
|
|
|
bind(this.els.gitlabTplSelect, "change", function (event) {
|
|
self.loadGitlabTemplate(event.target.value);
|
|
});
|
|
|
|
bind(this.els.addStep, "click", function (event) {
|
|
event.preventDefault();
|
|
event.stopImmediatePropagation();
|
|
self.addStep();
|
|
});
|
|
|
|
bind(this.els.addGroup, "click", function (event) {
|
|
event.preventDefault();
|
|
event.stopImmediatePropagation();
|
|
self.addGroup();
|
|
});
|
|
|
|
bind(this.els.saveJson, "click", function () {
|
|
self.saveJson();
|
|
});
|
|
|
|
bind(this.els.pickRun, "click", function () {
|
|
if (self.els.loadJson) {
|
|
self.els.loadJson.click();
|
|
}
|
|
});
|
|
|
|
bind(this.els.loadJson, "change", function (event) {
|
|
self.loadRunJson(event);
|
|
});
|
|
|
|
bind(this.els.exportAll, "click", function () {
|
|
self.exportAll();
|
|
});
|
|
|
|
bind(this.els.exportMd, "click", function () {
|
|
self.exportMd();
|
|
});
|
|
|
|
bind(this.els.exportCsv, "click", function () {
|
|
self.exportCsv();
|
|
});
|
|
|
|
bind(this.els.printPdf, "click", function () {
|
|
self.printPdf();
|
|
});
|
|
|
|
bind(this.els.exportTemplateYaml, "click", function () {
|
|
self.exportTemplateYaml();
|
|
});
|
|
|
|
bind(this.els.pushDocbee, "click", function () {
|
|
self.postToDocbee();
|
|
});
|
|
|
|
if (this.els.stepsTableBody) {
|
|
this.els.stepsTableBody.addEventListener("click", function (event) {
|
|
self.handleRowClick(event);
|
|
});
|
|
this.els.stepsTableBody.addEventListener("input", function () {
|
|
self.captureEditsIntoTemplate();
|
|
self.updateRunSummary();
|
|
});
|
|
this.els.stepsTableBody.addEventListener("change", function (event) {
|
|
if (event.target && event.target.matches("select.oqt-status")) {
|
|
self.updateStatusClass(event.target);
|
|
self.recomputeGroupStyles();
|
|
}
|
|
self.captureEditsIntoTemplate();
|
|
self.updateRunSummary();
|
|
});
|
|
this.els.stepsTableBody.addEventListener("dragstart", function (event) {
|
|
self.handleDragStart(event);
|
|
});
|
|
this.els.stepsTableBody.addEventListener("dragend", function (event) {
|
|
self.handleDragEnd(event);
|
|
});
|
|
this.els.stepsTableBody.addEventListener("dragover", function (event) {
|
|
self.handleDragOver(event);
|
|
});
|
|
this.els.stepsTableBody.addEventListener("drop", function (event) {
|
|
self.handleDrop(event);
|
|
});
|
|
}
|
|
};
|
|
|
|
QaTool.prototype.api = async function (path, options) {
|
|
options = options || {};
|
|
var response = await fetch((config.restUrl || "") + path, {
|
|
method: options.method || "GET",
|
|
credentials: "same-origin",
|
|
headers: Object.assign({
|
|
"Accept": "application/json",
|
|
"X-WP-Nonce": config.nonce || ""
|
|
}, options.headers || {}),
|
|
body: options.body || null
|
|
});
|
|
|
|
var text = await response.text();
|
|
var data = {};
|
|
if (text) {
|
|
try {
|
|
data = JSON.parse(text);
|
|
} catch (error) {
|
|
data = { message: text };
|
|
}
|
|
}
|
|
|
|
if (!response.ok) {
|
|
throw new Error(data.message || ("Request failed with HTTP " + response.status));
|
|
}
|
|
|
|
return data;
|
|
};
|
|
|
|
QaTool.prototype.populateGitlabDropdown = async function () {
|
|
var select = this.els.gitlabTplSelect;
|
|
var tag = this.els.gitlabTplStatus;
|
|
|
|
if (!select) {
|
|
return;
|
|
}
|
|
|
|
select.innerHTML = '<option value="">GitLab-Templates laden</option>';
|
|
|
|
if (!config.gitlabEnabled) {
|
|
select.disabled = true;
|
|
this.setTag(tag, "GitLab: deaktiviert", "warn");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
select.disabled = true;
|
|
this.setTag(tag, "GitLab: lade Liste", "");
|
|
var data = await this.api("templates");
|
|
var templates = data.templates || [];
|
|
|
|
templates.forEach(function (item) {
|
|
var option = document.createElement("option");
|
|
option.value = item.path;
|
|
option.textContent = item.name;
|
|
select.appendChild(option);
|
|
});
|
|
|
|
this.setTag(tag, templates.length ? ("GitLab: " + templates.length + " Vorlage(n)") : "GitLab: keine YAMLs", templates.length ? "ok" : "warn");
|
|
} catch (error) {
|
|
this.setTag(tag, "GitLab: Fehler", "bad");
|
|
this.showMessage("GitLab-Liste konnte nicht geladen werden: " + error.message, "bad");
|
|
} finally {
|
|
select.disabled = false;
|
|
}
|
|
};
|
|
|
|
QaTool.prototype.loadGitlabTemplate = async function (path) {
|
|
if (!path) {
|
|
this.gitlabTemplatePath = "";
|
|
this.gitlabTemplateModule = "";
|
|
return;
|
|
}
|
|
|
|
try {
|
|
this.setTag(this.els.gitlabTplStatus, "GitLab: lade Datei", "");
|
|
var data = await this.api("template?path=" + encodeURIComponent(path));
|
|
this.loadTemplateText(data.content || "", path.split("/").pop(), data.path || path);
|
|
this.setTag(this.els.gitlabTplStatus, "GitLab: geladen", "ok");
|
|
} catch (error) {
|
|
this.setTag(this.els.gitlabTplStatus, "GitLab: Fehler", "bad");
|
|
this.showMessage("Vorlage aus GitLab konnte nicht geladen werden: " + error.message, "bad");
|
|
}
|
|
};
|
|
|
|
QaTool.prototype.loadLocalTemplate = async function (event) {
|
|
var file = event.target.files && event.target.files[0];
|
|
if (!file) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
var text = await file.text();
|
|
this.loadTemplateText(text, file.name);
|
|
this.showMessage("Lokale Vorlage geladen.", "ok");
|
|
} catch (error) {
|
|
this.showMessage("Vorlage konnte nicht geladen werden: " + error.message, "bad");
|
|
} finally {
|
|
event.target.value = "";
|
|
}
|
|
};
|
|
|
|
QaTool.prototype.loadTemplateText = function (text, fallbackName, sourcePath) {
|
|
var parsed = parseTemplateText(text);
|
|
var normalized = normalizeTemplate(parsed, fallbackName || "Template");
|
|
this.template = normalized;
|
|
this.gitlabTemplatePath = sourcePath || "";
|
|
this.gitlabTemplateModule = sourcePath ? (normalized.module || "") : "";
|
|
|
|
if (this.els.tplName) {
|
|
this.els.tplName.textContent = normalized.name || fallbackName || "Template";
|
|
}
|
|
if (this.els.module) {
|
|
this.els.module.value = normalized.module || "";
|
|
}
|
|
if (this.els.moduleVersion) {
|
|
this.els.moduleVersion.value = normalized.module_version || "";
|
|
}
|
|
if (this.els.olmNummer) {
|
|
this.els.olmNummer.value = normalized.olm_nummer || "";
|
|
}
|
|
if (this.els.pbxVersion) {
|
|
this.els.pbxVersion.value = normalized.pbx_version || config.defaultPbxVersion || "";
|
|
}
|
|
|
|
this.renderSteps();
|
|
this.setTag(this.els.statusTag, "Vorlage geladen", "ok");
|
|
this.updateAutosave();
|
|
};
|
|
|
|
QaTool.prototype.renderSteps = function () {
|
|
var body = this.els.stepsTableBody;
|
|
if (!body || !this.template) {
|
|
return;
|
|
}
|
|
|
|
body.innerHTML = "";
|
|
var groupCollapsed = false;
|
|
var self = this;
|
|
|
|
(this.template.steps || []).forEach(function (step) {
|
|
var kind = step.kind || step.type || "step";
|
|
if (kind === "group") {
|
|
var groupRow = document.createElement("tr");
|
|
groupRow.className = "oqt-group-row";
|
|
groupRow.dataset.kind = "group";
|
|
groupRow.draggable = false;
|
|
if (step.collapsed) {
|
|
groupRow.dataset.collapsed = "1";
|
|
}
|
|
|
|
groupRow.innerHTML =
|
|
'<td colspan="4">' +
|
|
'<div class="oqt-group-head">' +
|
|
'<div class="oqt-group-main">' +
|
|
'<span class="oqt-group-label">Gruppe</span>' +
|
|
'<input class="oqt-group-title" type="text" value="' + escAttr(step.title || "") + '" placeholder="Gruppen-Titel">' +
|
|
'</div>' +
|
|
'<div class="oqt-group-actions">' +
|
|
'<div class="oqt-status-set" aria-label="Gruppenstatus setzen">' +
|
|
'<button type="button" class="oqt-status-btn is-pass" data-row-action="group-status" data-status="pass">PASS</button>' +
|
|
'<button type="button" class="oqt-status-btn is-fail" data-row-action="group-status" data-status="fail">FAIL</button>' +
|
|
'<button type="button" class="oqt-status-btn is-skip" data-row-action="group-status" data-status="skip">SKIP</button>' +
|
|
'<button type="button" class="oqt-status-btn is-blocked" data-row-action="group-status" data-status="blocked">BLOCK</button>' +
|
|
'</div>' +
|
|
'<div class="oqt-row-tools">' +
|
|
'<button type="button" class="oqt-icon-btn" data-row-action="toggle-group" title="' + (step.collapsed ? "Gruppe öffnen" : "Gruppe schließen") + '" aria-label="' + (step.collapsed ? "Gruppe öffnen" : "Gruppe schließen") + '">' +
|
|
'<span class="dashicons ' + (step.collapsed ? "dashicons-arrow-down-alt2" : "dashicons-arrow-up-alt2") + '" aria-hidden="true"></span>' +
|
|
'</button>' +
|
|
'<button type="button" class="oqt-icon-btn" data-row-action="delete-group" title="Gruppe löschen" aria-label="Gruppe löschen">' +
|
|
'<span class="dashicons dashicons-trash" aria-hidden="true"></span>' +
|
|
'</button>' +
|
|
'<span class="oqt-drag-handle" draggable="true" title="Ziehen zum Verschieben" aria-label="Ziehen zum Verschieben"><span class="dashicons dashicons-menu" aria-hidden="true"></span></span>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'</td>';
|
|
|
|
body.appendChild(groupRow);
|
|
groupCollapsed = !!step.collapsed;
|
|
return;
|
|
}
|
|
|
|
var row = document.createElement("tr");
|
|
row.dataset.kind = "step";
|
|
row.draggable = false;
|
|
if (groupCollapsed) {
|
|
row.dataset.hidden = "1";
|
|
}
|
|
|
|
row.innerHTML =
|
|
'<td>' +
|
|
'<div class="oqt-step-head">' +
|
|
'<div class="oqt-step-id-wrap">' +
|
|
'<span class="oqt-mini-label">ID</span>' +
|
|
'<input class="oqt-step-id" type="text" value="' + escAttr(step.id || "") + '" aria-label="Step ID">' +
|
|
'</div>' +
|
|
'<div class="oqt-step-title-wrap">' +
|
|
'<span class="oqt-mini-label">Titel</span>' +
|
|
'<textarea class="oqt-title" placeholder="Titel">' + escHtml(step.title || "") + '</textarea>' +
|
|
'</div>' +
|
|
'<div class="oqt-row-tools oqt-step-tools">' +
|
|
(step.required ? '<span class="oqt-pin">Pflicht</span>' : '<span class="oqt-pin oqt-pin--off">Optional</span>') +
|
|
'<button type="button" class="oqt-icon-btn" data-row-action="delete-step" title="Step löschen" aria-label="Step löschen">' +
|
|
'<span class="dashicons dashicons-trash" aria-hidden="true"></span>' +
|
|
'</button>' +
|
|
'<span class="oqt-drag-handle" draggable="true" title="Ziehen zum Verschieben" aria-label="Ziehen zum Verschieben"><span class="dashicons dashicons-menu" aria-hidden="true"></span></span>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'<label class="oqt-required"><input type="checkbox" class="oqt-step-required" ' + (step.required ? "checked" : "") + '> Pflichtschritt</label>' +
|
|
'</td>' +
|
|
'<td><textarea class="oqt-expected" placeholder="Erwartetes Verhalten">' + escHtml(step.expected || "") + '</textarea></td>' +
|
|
'<td>' +
|
|
'<select class="oqt-status">' +
|
|
'<option value="" ' + (!step.status ? "selected" : "") + '></option>' +
|
|
'<option value="pass" ' + (step.status === "pass" ? "selected" : "") + '>pass</option>' +
|
|
'<option value="fail" ' + (step.status === "fail" ? "selected" : "") + '>fail</option>' +
|
|
'<option value="skip" ' + (step.status === "skip" ? "selected" : "") + '>skip</option>' +
|
|
'<option value="blocked" ' + (step.status === "blocked" ? "selected" : "") + '>blocked</option>' +
|
|
'</select>' +
|
|
'</td>' +
|
|
'<td>' +
|
|
'<textarea class="oqt-comment" placeholder="Kommentar">' + escHtml(step.comment || "") + '</textarea>' +
|
|
'<input class="oqt-evidence" type="url" placeholder="Evidenz-URL" value="' + escAttr(step.evidence || "") + '">' +
|
|
'</td>';
|
|
|
|
body.appendChild(row);
|
|
self.updateStatusClass(row.querySelector("select.oqt-status"));
|
|
});
|
|
|
|
this.recomputeGroupStyles();
|
|
this.updateRunSummary();
|
|
};
|
|
|
|
QaTool.prototype.captureEditsIntoTemplate = function () {
|
|
if (!this.template || !this.els.stepsTableBody) {
|
|
return;
|
|
}
|
|
|
|
var rows = Array.prototype.slice.call(this.els.stepsTableBody.querySelectorAll("tr")).filter(function (row) {
|
|
return !row.classList.contains("oqt-empty-row");
|
|
});
|
|
this.template.steps = rows.map(function (row, index) {
|
|
var kind = row.dataset.kind || "step";
|
|
if (kind === "group") {
|
|
return {
|
|
kind: "group",
|
|
title: valueOf(row, ".oqt-group-title"),
|
|
collapsed: row.dataset.collapsed === "1"
|
|
};
|
|
}
|
|
|
|
return {
|
|
kind: "step",
|
|
id: valueOf(row, ".oqt-step-id") || ("step-" + String(index + 1).padStart(3, "0")),
|
|
title: valueOf(row, ".oqt-title"),
|
|
expected: valueOf(row, ".oqt-expected"),
|
|
required: !!(row.querySelector(".oqt-step-required") && row.querySelector(".oqt-step-required").checked),
|
|
status: valueOf(row, ".oqt-status"),
|
|
comment: valueOf(row, ".oqt-comment"),
|
|
evidence: valueOf(row, ".oqt-evidence")
|
|
};
|
|
});
|
|
};
|
|
|
|
QaTool.prototype.ensureTemplate = function () {
|
|
if (!this.template) {
|
|
this.template = {
|
|
name: "Neues Template",
|
|
module: "",
|
|
module_version: "",
|
|
olm_nummer: "",
|
|
pbx_version: config.defaultPbxVersion || "",
|
|
steps: []
|
|
};
|
|
this.gitlabTemplatePath = "";
|
|
this.gitlabTemplateModule = "";
|
|
if (this.els.tplName) {
|
|
this.els.tplName.textContent = this.template.name;
|
|
}
|
|
}
|
|
};
|
|
|
|
QaTool.prototype.addStep = function () {
|
|
var hadTemplate = !!this.template;
|
|
this.ensureTemplate();
|
|
if (hadTemplate) {
|
|
this.captureEditsIntoTemplate();
|
|
}
|
|
this.template.steps.push({
|
|
kind: "step",
|
|
id: "",
|
|
title: "",
|
|
expected: "",
|
|
required: true,
|
|
status: "",
|
|
comment: "",
|
|
evidence: ""
|
|
});
|
|
this.renumberSteps();
|
|
this.renderSteps();
|
|
this.setTag(this.els.statusTag, "Step hinzugefügt", "ok");
|
|
};
|
|
|
|
QaTool.prototype.addGroup = function () {
|
|
var hadTemplate = !!this.template;
|
|
this.ensureTemplate();
|
|
if (hadTemplate) {
|
|
this.captureEditsIntoTemplate();
|
|
}
|
|
this.template.steps.push({
|
|
kind: "group",
|
|
title: "Neue Gruppe",
|
|
collapsed: false
|
|
});
|
|
this.renderSteps();
|
|
this.setTag(this.els.statusTag, "Gruppe hinzugefügt", "ok");
|
|
};
|
|
|
|
QaTool.prototype.handleRowClick = function (event) {
|
|
var button = event.target.closest("[data-row-action]");
|
|
if (!button) {
|
|
return;
|
|
}
|
|
|
|
event.preventDefault();
|
|
this.captureEditsIntoTemplate();
|
|
|
|
var row = button.closest("tr");
|
|
var index = this.rowIndex(row);
|
|
if (index < 0 || !this.template || !this.template.steps[index]) {
|
|
return;
|
|
}
|
|
|
|
var action = button.dataset.rowAction;
|
|
var item = this.template.steps[index];
|
|
|
|
if (action === "toggle-group" && item.kind === "group") {
|
|
item.collapsed = !item.collapsed;
|
|
this.renderSteps();
|
|
return;
|
|
}
|
|
|
|
if (action === "group-status" && item.kind === "group") {
|
|
this.applyGroupStatus(index, button.dataset.status || "");
|
|
return;
|
|
}
|
|
|
|
if (action === "delete-group" && item.kind === "group") {
|
|
var count = this.countStepsUntilNextGroup(index);
|
|
if (count > 0 && !window.confirm("Diese Gruppe enthält " + count + " Schritt(e). Gruppe wirklich löschen? Die Schritte bleiben erhalten.")) {
|
|
return;
|
|
}
|
|
this.template.steps.splice(index, 1);
|
|
this.renderSteps();
|
|
return;
|
|
}
|
|
|
|
if (action === "delete-step") {
|
|
this.template.steps.splice(index, 1);
|
|
this.renumberSteps();
|
|
this.renderSteps();
|
|
}
|
|
};
|
|
|
|
QaTool.prototype.applyGroupStatus = function (groupIndex, status) {
|
|
if (["pass", "fail", "skip", "blocked"].indexOf(status) === -1) {
|
|
return;
|
|
}
|
|
|
|
var end = this.template.steps.length;
|
|
for (var i = groupIndex + 1; i < this.template.steps.length; i++) {
|
|
var kind = this.template.steps[i].kind || this.template.steps[i].type || "step";
|
|
if (kind === "group") {
|
|
end = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
var willOverwrite = this.template.steps.slice(groupIndex + 1, end).some(function (step) {
|
|
return (step.kind || "step") === "step" && step.status && step.status !== status;
|
|
});
|
|
|
|
if (willOverwrite && !window.confirm("Einige Schritte haben bereits einen anderen Status. Alle auf " + status.toUpperCase() + " setzen?")) {
|
|
return;
|
|
}
|
|
|
|
for (var j = groupIndex + 1; j < end; j++) {
|
|
if ((this.template.steps[j].kind || "step") === "step") {
|
|
this.template.steps[j].status = status;
|
|
}
|
|
}
|
|
|
|
this.renderSteps();
|
|
};
|
|
|
|
QaTool.prototype.countStepsUntilNextGroup = function (groupIndex) {
|
|
var count = 0;
|
|
for (var i = groupIndex + 1; i < this.template.steps.length; i++) {
|
|
var kind = this.template.steps[i].kind || this.template.steps[i].type || "step";
|
|
if (kind === "group") {
|
|
break;
|
|
}
|
|
if (kind === "step") {
|
|
count++;
|
|
}
|
|
}
|
|
return count;
|
|
};
|
|
|
|
QaTool.prototype.rowIndex = function (row) {
|
|
if (!row || !this.els.stepsTableBody) {
|
|
return -1;
|
|
}
|
|
return Array.prototype.indexOf.call(this.els.stepsTableBody.children, row);
|
|
};
|
|
|
|
QaTool.prototype.handleDragStart = function (event) {
|
|
if (!event.target.closest(".oqt-drag-handle")) {
|
|
event.preventDefault();
|
|
return;
|
|
}
|
|
|
|
var row = event.target.closest("tr");
|
|
var index = this.rowIndex(row);
|
|
if (index < 0) {
|
|
return;
|
|
}
|
|
|
|
this.dragIndex = index;
|
|
row.classList.add("oqt-dragging");
|
|
if (event.dataTransfer) {
|
|
event.dataTransfer.effectAllowed = "move";
|
|
event.dataTransfer.setData("text/plain", String(index));
|
|
}
|
|
};
|
|
|
|
QaTool.prototype.handleDragEnd = function (event) {
|
|
var row = event.target.closest("tr");
|
|
if (row) {
|
|
row.classList.remove("oqt-dragging");
|
|
}
|
|
this.clearDragMarkers();
|
|
this.dragIndex = -1;
|
|
};
|
|
|
|
QaTool.prototype.handleDragOver = function (event) {
|
|
event.preventDefault();
|
|
var row = event.target.closest("tr");
|
|
if (!row) {
|
|
return;
|
|
}
|
|
|
|
this.clearDragMarkers();
|
|
row.classList.add("oqt-drag-over");
|
|
var rect = row.getBoundingClientRect();
|
|
row.dataset.dropPos = (event.clientY - rect.top) < rect.height / 2 ? "before" : "after";
|
|
};
|
|
|
|
QaTool.prototype.handleDrop = function (event) {
|
|
event.preventDefault();
|
|
var row = event.target.closest("tr");
|
|
if (!row || !this.template) {
|
|
return;
|
|
}
|
|
|
|
this.captureEditsIntoTemplate();
|
|
var from = this.dragIndex >= 0 ? this.dragIndex : parseInt(event.dataTransfer && event.dataTransfer.getData("text/plain"), 10);
|
|
var toBase = this.rowIndex(row);
|
|
if (!isFinite(from) || from < 0 || toBase < 0) {
|
|
return;
|
|
}
|
|
|
|
var to = toBase + (row.dataset.dropPos === "after" ? 1 : 0);
|
|
if (to === from || to - 1 === from) {
|
|
this.renderSteps();
|
|
return;
|
|
}
|
|
|
|
var item = this.template.steps.splice(from, 1)[0];
|
|
if (to > from) {
|
|
to--;
|
|
}
|
|
this.template.steps.splice(to, 0, item);
|
|
this.renumberSteps();
|
|
this.renderSteps();
|
|
};
|
|
|
|
QaTool.prototype.clearDragMarkers = function () {
|
|
if (!this.els.stepsTableBody) {
|
|
return;
|
|
}
|
|
this.els.stepsTableBody.querySelectorAll("tr").forEach(function (row) {
|
|
row.classList.remove("oqt-drag-over");
|
|
delete row.dataset.dropPos;
|
|
});
|
|
};
|
|
|
|
QaTool.prototype.updateStatusClass = function (select) {
|
|
if (!select) {
|
|
return;
|
|
}
|
|
var row = select.closest("tr");
|
|
["oqt-status-pass", "oqt-status-fail", "oqt-status-skip", "oqt-status-blocked"].forEach(function (className) {
|
|
select.classList.remove(className);
|
|
});
|
|
["oqt-row-pass", "oqt-row-fail", "oqt-row-skip", "oqt-row-blocked"].forEach(function (className) {
|
|
if (row) {
|
|
row.classList.remove(className);
|
|
}
|
|
});
|
|
|
|
if (!select.value) {
|
|
return;
|
|
}
|
|
|
|
select.classList.add("oqt-status-" + select.value);
|
|
if (row) {
|
|
row.classList.add("oqt-row-" + select.value);
|
|
}
|
|
};
|
|
|
|
QaTool.prototype.recomputeGroupStyles = function () {
|
|
if (!this.els.stepsTableBody) {
|
|
return;
|
|
}
|
|
|
|
var current = null;
|
|
var groups = [];
|
|
this.els.stepsTableBody.querySelectorAll("tr").forEach(function (row) {
|
|
if ((row.dataset.kind || "step") === "group") {
|
|
current = { row: row, steps: [] };
|
|
groups.push(current);
|
|
} else if (current) {
|
|
current.steps.push(row);
|
|
}
|
|
});
|
|
|
|
groups.forEach(function (group) {
|
|
group.row.classList.remove("is-ok", "is-bad");
|
|
var statuses = group.steps.map(function (row) {
|
|
var select = row.querySelector("select.oqt-status");
|
|
return select ? select.value : "";
|
|
});
|
|
if (!statuses.length) {
|
|
return;
|
|
}
|
|
if (statuses.every(function (status) { return status === "pass"; })) {
|
|
group.row.classList.add("is-ok");
|
|
} else if (statuses.some(function (status) { return status === "fail" || status === "blocked"; })) {
|
|
group.row.classList.add("is-bad");
|
|
}
|
|
});
|
|
};
|
|
|
|
QaTool.prototype.updateRunSummary = function () {
|
|
if (!this.els.stepSummary) {
|
|
return;
|
|
}
|
|
|
|
var counts = countStatuses(this.template ? this.template.steps : []);
|
|
if (!counts.total) {
|
|
this.setTag(this.els.stepSummary, "0 Steps", "");
|
|
return;
|
|
}
|
|
|
|
var text = counts.total + " Step" + (counts.total === 1 ? "" : "s") +
|
|
" | " + counts.pass + " PASS" +
|
|
" | " + counts.fail + " FAIL" +
|
|
" | " + counts.skip + " SKIP" +
|
|
" | " + counts.blocked + " BLOCK";
|
|
if (counts.missingRequired) {
|
|
text += " | " + counts.missingRequired + " offen";
|
|
}
|
|
this.setTag(this.els.stepSummary, text, counts.missingRequired ? "warn" : (counts.total ? "ok" : ""));
|
|
};
|
|
|
|
QaTool.prototype.renumberSteps = function () {
|
|
if (!this.template || !Array.isArray(this.template.steps)) {
|
|
return;
|
|
}
|
|
|
|
var number = 1;
|
|
this.template.steps.forEach(function (step) {
|
|
var kind = step.kind || step.type || "step";
|
|
if (kind !== "step") {
|
|
return;
|
|
}
|
|
step.id = "step-" + String(number).padStart(3, "0");
|
|
number++;
|
|
});
|
|
};
|
|
|
|
QaTool.prototype.collectRun = function () {
|
|
if (!this.template) {
|
|
this.showMessage("Bitte zuerst eine Vorlage laden oder Schritte anlegen.", "bad");
|
|
return null;
|
|
}
|
|
|
|
this.captureEditsIntoTemplate();
|
|
this.renumberSteps();
|
|
return {
|
|
name: this.template.name || "",
|
|
module: value(this.els.module),
|
|
module_version: value(this.els.moduleVersion),
|
|
olm_nummer: value(this.els.olmNummer),
|
|
pbx_version: value(this.els.pbxVersion),
|
|
tester: value(this.els.tester),
|
|
docbee_url: value(this.els.docbeeUrl),
|
|
ts: new Date().toISOString(),
|
|
steps: clone(this.template.steps || [])
|
|
};
|
|
};
|
|
|
|
QaTool.prototype.collectTemplateFromDom = function () {
|
|
this.ensureTemplate();
|
|
this.captureEditsIntoTemplate();
|
|
this.renumberSteps();
|
|
|
|
return {
|
|
name: this.template.name || "QA Template",
|
|
module: value(this.els.module),
|
|
module_version: value(this.els.moduleVersion),
|
|
olm_nummer: value(this.els.olmNummer),
|
|
pbx_version: value(this.els.pbxVersion),
|
|
steps: (this.template.steps || []).map(function (step) {
|
|
var kind = step.kind || step.type || "step";
|
|
if (kind === "group") {
|
|
return { type: "group", title: step.title || "" };
|
|
}
|
|
return {
|
|
type: "step",
|
|
id: step.id || "",
|
|
title: step.title || "",
|
|
expected: step.expected || "",
|
|
required: !!step.required
|
|
};
|
|
})
|
|
};
|
|
};
|
|
|
|
QaTool.prototype.checkRequired = function (run) {
|
|
var missing = (run.steps || []).filter(function (step) {
|
|
return (step.kind || step.type || "step") === "step" && step.required && !step.status;
|
|
});
|
|
|
|
if (!missing.length) {
|
|
return true;
|
|
}
|
|
|
|
var message = "Folgende Pflichtschritte haben keinen Status:\n" + missing.map(function (step) {
|
|
return (step.id || "") + " - " + (step.title || "");
|
|
}).join("\n");
|
|
window.alert(message);
|
|
this.showMessage("Pflichtschritte ohne Status vorhanden.", "bad");
|
|
return false;
|
|
};
|
|
|
|
QaTool.prototype.saveJson = function () {
|
|
var run = this.collectRun();
|
|
if (!run || !this.checkRequired(run)) {
|
|
return;
|
|
}
|
|
this.renumberSteps();
|
|
download(JSON.stringify(run, null, 2), runBaseName("qa-run", run) + ".json", "application/json");
|
|
};
|
|
|
|
QaTool.prototype.loadRunJson = async function (event) {
|
|
var file = event.target.files && event.target.files[0];
|
|
if (!file) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
var text = await file.text();
|
|
var run = parseTemplateText(text);
|
|
var isRun = !!(run.tester || run.docbee_url || (run.steps || []).some(function (step) {
|
|
return step && (step.status || step.comment || step.evidence);
|
|
}));
|
|
if (!run || !Array.isArray(run.steps)) {
|
|
throw new Error("Datei enthält keine steps.");
|
|
}
|
|
|
|
this.template = normalizeTemplate({
|
|
name: run.name || file.name,
|
|
module: run.module || "",
|
|
module_version: run.module_version || "",
|
|
olm_nummer: run.olm_nummer || "",
|
|
pbx_version: run.pbx_version || "",
|
|
steps: run.steps
|
|
}, file.name);
|
|
this.gitlabTemplatePath = "";
|
|
this.gitlabTemplateModule = "";
|
|
|
|
if (this.els.tplName) {
|
|
this.els.tplName.textContent = run.name || file.name;
|
|
}
|
|
if (this.els.module) {
|
|
this.els.module.value = run.module || "";
|
|
}
|
|
if (this.els.moduleVersion) {
|
|
this.els.moduleVersion.value = run.module_version || "";
|
|
}
|
|
if (this.els.olmNummer) {
|
|
this.els.olmNummer.value = run.olm_nummer || "";
|
|
}
|
|
if (this.els.pbxVersion) {
|
|
this.els.pbxVersion.value = run.pbx_version || "";
|
|
}
|
|
if (this.els.tester) {
|
|
this.els.tester.value = isRun ? (run.tester || config.tester || "") : (config.tester || "");
|
|
}
|
|
if (this.els.docbeeUrl) {
|
|
this.els.docbeeUrl.value = isRun ? (run.docbee_url || "") : "";
|
|
}
|
|
|
|
this.renderSteps();
|
|
this.setTag(this.els.statusTag, isRun ? "Lauf geladen" : "Template geladen", "ok");
|
|
this.updateAutosave();
|
|
} catch (error) {
|
|
this.showMessage("Datei konnte nicht geladen werden: " + error.message, "bad");
|
|
} finally {
|
|
event.target.value = "";
|
|
}
|
|
};
|
|
|
|
QaTool.prototype.exportMd = function () {
|
|
var run = this.collectRun();
|
|
if (!run || !this.checkRequired(run)) {
|
|
return;
|
|
}
|
|
this.renumberSteps();
|
|
|
|
var md = "# Testprotokoll\n\n" +
|
|
"- Modul: " + (run.module || "") + "\n" +
|
|
"- Modul-Version: " + (run.module_version || "") + "\n" +
|
|
"- OLM-Nummer: " + (run.olm_nummer || "") + "\n" +
|
|
"- PBX-Version: " + (run.pbx_version || "") + "\n" +
|
|
"- DocBee: " + (run.docbee_url || "") + "\n" +
|
|
"- Tester: " + (run.tester || "") + "\n" +
|
|
"- Datum: " + new Date(run.ts).toLocaleString("de-DE") + "\n\n" +
|
|
"| Schritt | Erwartung | Status | Kommentar | Evidenz |\n" +
|
|
"|---|---|---|---|---|\n";
|
|
|
|
run.steps.forEach(function (step) {
|
|
if ((step.kind || step.type || "step") === "group") {
|
|
md += "| **-- " + mdCell(step.title) + " --** | | | | |\n";
|
|
return;
|
|
}
|
|
md += "| **" + mdCell(step.id) + " - " + mdCell(step.title) + (step.required ? " [required]" : "") + "** | " +
|
|
mdCell(step.expected) + " | " + mdCell(step.status) + " | " + mdCell(step.comment) + " | " + mdCell(step.evidence) + " |\n";
|
|
});
|
|
|
|
download(md, runBaseName("qa-report", run) + ".md", "text/markdown");
|
|
};
|
|
|
|
QaTool.prototype.exportCsv = function () {
|
|
var run = this.collectRun();
|
|
if (!run || !this.checkRequired(run)) {
|
|
return;
|
|
}
|
|
this.renumberSteps();
|
|
|
|
var meta = [
|
|
["module", run.module || ""],
|
|
["module_version", run.module_version || ""],
|
|
["olm_nummer", run.olm_nummer || ""],
|
|
["pbx_version", run.pbx_version || ""],
|
|
["docbee_url", run.docbee_url || ""],
|
|
["tester", run.tester || ""],
|
|
["date", new Date(run.ts).toLocaleString("de-DE")]
|
|
].map(function (row) {
|
|
return row[0] + ";" + csvCell(row[1]);
|
|
}).join("\n");
|
|
|
|
var rows = [["id", "title", "expected", "required", "status", "comment", "evidence"]];
|
|
run.steps.forEach(function (step) {
|
|
if ((step.kind || step.type || "step") === "group") {
|
|
rows.push(["", "### Gruppe: " + (step.title || ""), "", "", "", "", ""]);
|
|
return;
|
|
}
|
|
rows.push([step.id, step.title, step.expected, step.required ? "true" : "false", step.status, step.comment, step.evidence]);
|
|
});
|
|
|
|
var table = rows.map(function (row) {
|
|
return row.map(csvCell).join(";");
|
|
}).join("\n");
|
|
|
|
download(meta + "\n\n" + table, runBaseName("qa-report", run) + ".csv", "text/csv");
|
|
};
|
|
|
|
QaTool.prototype.ensureJsPdf = async function () {
|
|
if (window.jspdf && window.jspdf.jsPDF) {
|
|
return true;
|
|
}
|
|
|
|
var urls = config.jsPdfUrls || [];
|
|
for (var i = 0; i < urls.length; i++) {
|
|
try {
|
|
await loadScript(urls[i]);
|
|
} catch (error) {
|
|
// Try next CDN.
|
|
}
|
|
if (window.jspdf && window.jspdf.jsPDF) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!(window.jspdf && window.jspdf.jsPDF)) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
var test = new window.jspdf.jsPDF();
|
|
if (typeof test.autoTable !== "function") {
|
|
var tableUrls = config.autoTableUrls || [];
|
|
for (var j = 0; j < tableUrls.length; j++) {
|
|
try {
|
|
await loadScript(tableUrls[j]);
|
|
} catch (error2) {
|
|
// PDF export still works without AutoTable.
|
|
}
|
|
test = new window.jspdf.jsPDF();
|
|
if (typeof test.autoTable === "function") {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
} catch (error3) {
|
|
// Basic jsPDF is enough for a fallback PDF.
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
QaTool.prototype.generatePdfBlob = async function (run) {
|
|
var ready = await this.ensureJsPdf();
|
|
if (!ready) {
|
|
throw new Error("PDF-Bibliothek konnte nicht geladen werden.");
|
|
}
|
|
|
|
var jsPDF = window.jspdf.jsPDF;
|
|
var doc = new jsPDF({
|
|
orientation: "landscape",
|
|
unit: "mm",
|
|
format: "a4",
|
|
compress: true
|
|
});
|
|
|
|
var pageW = doc.internal.pageSize.getWidth();
|
|
var pageH = doc.internal.pageSize.getHeight();
|
|
var marginL = 14;
|
|
var marginR = 14;
|
|
var xRight = pageW - marginR;
|
|
var footerY = pageH - 13;
|
|
var muted = [102, 102, 102];
|
|
var headFill = [232, 247, 253];
|
|
var ok = [24, 143, 92];
|
|
var fail = [196, 59, 59];
|
|
var skip = [103, 113, 122];
|
|
var blocked = [245, 156, 0];
|
|
var logoDataUrl = null;
|
|
|
|
try {
|
|
logoDataUrl = await imageToDataUrl(config.printLogoUrl || config.logoUrl || "");
|
|
} catch (error) {
|
|
logoDataUrl = null;
|
|
}
|
|
|
|
doc.setFont("helvetica", "bold");
|
|
doc.setFontSize(18);
|
|
doc.setTextColor(52, 53, 55);
|
|
doc.text("QA Report", marginL, 16);
|
|
if (logoDataUrl) {
|
|
try {
|
|
var props = doc.getImageProperties ? doc.getImageProperties(logoDataUrl) : null;
|
|
var ratio = props && props.width ? props.height / props.width : 0.38;
|
|
var logoW = 32;
|
|
var logoH = logoW * ratio;
|
|
if (logoH > 13) {
|
|
logoH = 13;
|
|
logoW = logoH / ratio;
|
|
}
|
|
doc.addImage(logoDataUrl, "PNG", xRight - logoW, 6.5, logoW, logoH, undefined, "FAST");
|
|
} catch (error2) {
|
|
// Logo is decorative; keep the PDF export working without it.
|
|
}
|
|
}
|
|
doc.setDrawColor(0, 167, 230);
|
|
doc.setLineWidth(0.45);
|
|
doc.line(marginL, 18, xRight, 18);
|
|
|
|
var meta = [
|
|
["Modul", sanitizePdfText(run.module)],
|
|
["Modul-Version", sanitizePdfText(run.module_version)],
|
|
["OLM-Nummer", sanitizePdfText(run.olm_nummer || "-")],
|
|
["PBX-Version", sanitizePdfText(run.pbx_version)],
|
|
["Tester", sanitizePdfText(run.tester)],
|
|
["DocBee", sanitizePdfText(run.docbee_url || "-")]
|
|
];
|
|
var y = 25;
|
|
doc.setFontSize(10);
|
|
meta.forEach(function (row) {
|
|
doc.setFont("helvetica", "bold");
|
|
doc.setTextColor(muted[0], muted[1], muted[2]);
|
|
doc.text(row[0] + ":", marginL, y);
|
|
doc.setFont("helvetica", "normal");
|
|
doc.setTextColor(0, 0, 0);
|
|
doc.text(row[1] || "-", 50, y);
|
|
y += 6;
|
|
});
|
|
|
|
var rows = [];
|
|
var currentGroup = "-";
|
|
var index = 0;
|
|
(run.steps || []).forEach(function (step) {
|
|
if ((step.kind || step.type || "step") === "group") {
|
|
currentGroup = sanitizePdfText(step.title || "-");
|
|
return;
|
|
}
|
|
index++;
|
|
rows.push({
|
|
nr: index,
|
|
group: currentGroup,
|
|
id: sanitizePdfText(step.id),
|
|
title: sanitizePdfText(step.title) + (step.required ? " [required]" : ""),
|
|
expected: sanitizePdfText(step.expected),
|
|
status: sanitizePdfText((step.status || "").toLowerCase()),
|
|
comment: sanitizePdfText(step.comment)
|
|
});
|
|
});
|
|
|
|
if (typeof doc.autoTable === "function") {
|
|
doc.autoTable({
|
|
startY: Math.max(y + 4, 25),
|
|
tableWidth: pageW - (marginL + marginR),
|
|
head: [["#", "Gruppe", "Step-ID", "Titel", "Erwartet", "Status", "Kommentar"]],
|
|
body: rows.map(function (row) {
|
|
return [row.nr, row.group, row.id, row.title, row.expected, row.status, row.comment];
|
|
}),
|
|
styles: {
|
|
font: "helvetica",
|
|
fontSize: 9,
|
|
cellPadding: 2,
|
|
overflow: "linebreak",
|
|
valign: "top"
|
|
},
|
|
headStyles: {
|
|
fillColor: headFill,
|
|
textColor: [52, 53, 55],
|
|
halign: "left"
|
|
},
|
|
columnStyles: {
|
|
0: { halign: "right", cellWidth: 8 },
|
|
1: { cellWidth: 36 },
|
|
2: { cellWidth: 24 },
|
|
3: { cellWidth: 62 },
|
|
4: { cellWidth: 76, font: "courier", fillColor: [250, 250, 250] },
|
|
5: { cellWidth: 20 },
|
|
6: { cellWidth: 36 }
|
|
},
|
|
didParseCell: function (data) {
|
|
if (data.section === "body" && data.column.index === 5) {
|
|
var value = String(data.cell.raw || "").toLowerCase();
|
|
if (value === "pass") {
|
|
data.cell.styles.textColor = ok;
|
|
} else if (value === "fail") {
|
|
data.cell.styles.textColor = fail;
|
|
} else if (value === "skip" || value === "na") {
|
|
data.cell.styles.textColor = skip;
|
|
} else if (value === "blocked") {
|
|
data.cell.styles.textColor = blocked;
|
|
}
|
|
}
|
|
},
|
|
margin: { left: marginL, right: marginR },
|
|
pageBreak: "auto"
|
|
});
|
|
} else {
|
|
doc.setFont("helvetica", "normal");
|
|
doc.setFontSize(9);
|
|
y += 6;
|
|
rows.forEach(function (row) {
|
|
if (y > pageH - 20) {
|
|
doc.addPage();
|
|
y = 16;
|
|
}
|
|
doc.text([row.nr + " " + row.id + " " + row.status, row.group + " | " + row.title, row.expected, row.comment].join(" - "), marginL, y, {
|
|
maxWidth: pageW - marginL - marginR
|
|
});
|
|
y += 8;
|
|
});
|
|
}
|
|
|
|
var pageCount = doc.getNumberOfPages();
|
|
var timestamp = new Date().toLocaleString("de-DE");
|
|
for (var p = 1; p <= pageCount; p++) {
|
|
doc.setPage(p);
|
|
doc.setFont("helvetica", "normal");
|
|
doc.setFontSize(8);
|
|
doc.setTextColor(muted[0], muted[1], muted[2]);
|
|
doc.text("Seite " + p + " / " + pageCount, xRight, footerY, { align: "right" });
|
|
doc.text(timestamp, marginL, footerY);
|
|
}
|
|
|
|
return doc.output("blob");
|
|
};
|
|
|
|
QaTool.prototype.printPdf = async function () {
|
|
var run = this.collectRun();
|
|
if (!run || !this.checkRequired(run)) {
|
|
return;
|
|
}
|
|
this.renumberSteps();
|
|
|
|
try {
|
|
var pdfBlob = await this.generatePdfBlob(run);
|
|
download(pdfBlob, runBaseName("qa-report", run) + ".pdf", "application/pdf");
|
|
this.showMessage("PDF wurde erzeugt.", "ok");
|
|
return;
|
|
} catch (error) {
|
|
this.showMessage("PDF konnte nicht erzeugt werden: " + error.message, "bad");
|
|
return;
|
|
}
|
|
};
|
|
|
|
QaTool.prototype.exportAll = async function () {
|
|
var run = this.collectRun();
|
|
if (!run || !this.checkRequired(run)) {
|
|
return;
|
|
}
|
|
|
|
var button = this.els.exportAll;
|
|
try {
|
|
if (button) {
|
|
button.disabled = true;
|
|
button.classList.add("is-busy");
|
|
}
|
|
|
|
this.showMessage("Export läuft: PDF wird erzeugt und WordPress speichert den Lauf...", "");
|
|
var pdfBlob = null;
|
|
try {
|
|
pdfBlob = await this.generatePdfBlob(run);
|
|
} catch (pdfError) {
|
|
this.showMessage("PDF konnte nicht erzeugt werden, Report wird trotzdem gespeichert: " + pdfError.message, "bad");
|
|
}
|
|
|
|
var form = new FormData();
|
|
form.append("run", JSON.stringify(run));
|
|
if (pdfBlob) {
|
|
form.append("pdf", pdfBlob, runBaseName("qa-report", run) + ".pdf");
|
|
}
|
|
|
|
var data = await this.api("export", {
|
|
method: "POST",
|
|
body: form
|
|
});
|
|
|
|
var parts = [];
|
|
if (data.report_id) {
|
|
parts.push("Report-ID: " + data.report_id);
|
|
}
|
|
if (data.pdf_stored) {
|
|
parts.push("PDF geschuetzt gespeichert");
|
|
}
|
|
if (data.docbee && data.docbee.ok) {
|
|
var docbeeLink = data.docbee.url ? ' <a href="' + escAttr(data.docbee.url) + '" target="_blank" rel="noopener">DocBee öffnen</a>' : "";
|
|
parts.push("DocBee gepostet." + docbeeLink);
|
|
} else if (data.docbee && data.docbee.message) {
|
|
parts.push("DocBee Fehler: " + escHtml(data.docbee.message));
|
|
}
|
|
|
|
this.showMessage("Export fertig. " + parts.join(" | "), "ok", true);
|
|
} catch (error) {
|
|
this.showMessage("Export fehlgeschlagen: " + error.message, "bad");
|
|
} finally {
|
|
if (button) {
|
|
button.disabled = false;
|
|
button.classList.remove("is-busy");
|
|
}
|
|
}
|
|
};
|
|
|
|
QaTool.prototype.exportTemplateYaml = async function () {
|
|
var template = this.collectTemplateFromDom();
|
|
if (!template.module || !template.module_version || !template.pbx_version) {
|
|
this.showMessage("Bitte Modul, Modul-Version und PBX-Version angeben.", "bad");
|
|
return;
|
|
}
|
|
|
|
var yaml = dumpTemplateYaml(template);
|
|
var button = this.els.exportTemplateYaml;
|
|
|
|
if (!config.gitlabWritesEnabled) {
|
|
download(yaml, runBaseName("qa-template", template) + ".yaml", "text/yaml");
|
|
this.showMessage("GitLab-Schreiben ist deaktiviert. Template wurde lokal gespeichert.", "ok");
|
|
this.renderSteps();
|
|
return;
|
|
}
|
|
|
|
try {
|
|
if (button) {
|
|
button.disabled = true;
|
|
button.classList.add("is-busy");
|
|
}
|
|
this.showMessage("GitLab: Template wird geschrieben...", "");
|
|
var sourcePath = this.gitlabTemplatePath && sameModuleName(template.module, this.gitlabTemplateModule)
|
|
? this.gitlabTemplatePath
|
|
: "";
|
|
var data = await this.api("gitlab/template", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ template: template, yaml: yaml, source_path: sourcePath })
|
|
});
|
|
this.gitlabTemplatePath = data.path || sourcePath || "";
|
|
this.gitlabTemplateModule = template.module || "";
|
|
this.showMessage("GitLab: Template gespeichert: " + (data.path || ""), "ok");
|
|
} catch (error) {
|
|
this.showMessage("GitLab-Push fehlgeschlagen: " + error.message, "bad");
|
|
} finally {
|
|
if (button) {
|
|
button.disabled = false;
|
|
button.classList.remove("is-busy");
|
|
}
|
|
this.renderSteps();
|
|
}
|
|
};
|
|
|
|
QaTool.prototype.postToDocbee = async function () {
|
|
var run = this.collectRun();
|
|
if (!run || !this.checkRequired(run)) {
|
|
return;
|
|
}
|
|
|
|
if (!config.docbeeEnabled || !config.docbeeConfigured) {
|
|
this.showMessage("DocBee ist im WordPress Backend nicht vollständig konfiguriert.", "bad");
|
|
return;
|
|
}
|
|
|
|
var button = this.els.pushDocbee;
|
|
try {
|
|
if (button) {
|
|
button.disabled = true;
|
|
button.classList.add("is-busy");
|
|
}
|
|
this.showMessage("DocBee: sende Report...", "");
|
|
var data = await this.api("docbee/message", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ run: run })
|
|
});
|
|
|
|
var suffix = data.url ? ' <a href="' + escAttr(data.url) + '" target="_blank" rel="noopener">Oeffnen</a>' : "";
|
|
this.showMessage("DocBee: Report wurde gepostet." + suffix, "ok", true);
|
|
} catch (error) {
|
|
this.showMessage("DocBee-Request fehlgeschlagen: " + error.message, "bad");
|
|
} finally {
|
|
if (button) {
|
|
button.disabled = false;
|
|
button.classList.remove("is-busy");
|
|
}
|
|
}
|
|
};
|
|
|
|
QaTool.prototype.updateAutosave = function () {
|
|
var state = {
|
|
module: value(this.els.module),
|
|
moduleVersion: value(this.els.moduleVersion),
|
|
olmNummer: value(this.els.olmNummer),
|
|
pbxVersion: value(this.els.pbxVersion),
|
|
docbeeUrl: value(this.els.docbeeUrl)
|
|
};
|
|
|
|
try {
|
|
window.localStorage.setItem(this.storageKey, JSON.stringify(state));
|
|
} catch (error) {
|
|
// Ignore private browsing quota errors.
|
|
}
|
|
};
|
|
|
|
QaTool.prototype.restoreAutosave = function () {
|
|
try {
|
|
var state = JSON.parse(window.localStorage.getItem(this.storageKey) || "{}");
|
|
if (state.module && this.els.module) {
|
|
this.els.module.value = state.module;
|
|
}
|
|
if (state.moduleVersion && this.els.moduleVersion) {
|
|
this.els.moduleVersion.value = state.moduleVersion;
|
|
}
|
|
if (state.olmNummer && this.els.olmNummer) {
|
|
this.els.olmNummer.value = state.olmNummer;
|
|
}
|
|
if (state.pbxVersion && this.els.pbxVersion) {
|
|
this.els.pbxVersion.value = state.pbxVersion;
|
|
}
|
|
if (state.docbeeUrl && this.els.docbeeUrl) {
|
|
this.els.docbeeUrl.value = state.docbeeUrl;
|
|
}
|
|
} catch (error) {
|
|
// Ignore broken old autosave data.
|
|
}
|
|
};
|
|
|
|
QaTool.prototype.updateServiceBadges = function () {
|
|
if (config.gitlabEnabled) {
|
|
this.setTag(this.els.gitlabTplStatus, "GitLab: bereit", "");
|
|
} else {
|
|
this.setTag(this.els.gitlabTplStatus, "GitLab: deaktiviert", "warn");
|
|
}
|
|
|
|
if (!config.docbeeEnabled) {
|
|
this.setTag(this.els.docbeeTokenStatus, "DocBee: deaktiviert", "warn");
|
|
} else if (config.docbeeConfigured) {
|
|
this.setTag(this.els.docbeeTokenStatus, "DocBee: konfiguriert", "ok");
|
|
} else {
|
|
this.setTag(this.els.docbeeTokenStatus, "DocBee: fehlt", "warn");
|
|
}
|
|
};
|
|
|
|
QaTool.prototype.setTag = function (tag, text, state) {
|
|
if (!tag) {
|
|
return;
|
|
}
|
|
tag.textContent = text;
|
|
tag.classList.remove("is-ok", "is-warn", "is-bad");
|
|
if (state) {
|
|
tag.classList.add("is-" + state);
|
|
}
|
|
};
|
|
|
|
QaTool.prototype.showMessage = function (message, state, allowHtml) {
|
|
if (!this.els.message) {
|
|
return;
|
|
}
|
|
this.els.message.classList.remove("is-ok", "is-bad");
|
|
if (state) {
|
|
this.els.message.classList.add("is-" + state);
|
|
}
|
|
if (allowHtml) {
|
|
this.els.message.innerHTML = message;
|
|
} else {
|
|
this.els.message.textContent = message;
|
|
}
|
|
};
|
|
|
|
function bind(el, eventName, callback) {
|
|
if (el) {
|
|
el.addEventListener(eventName, callback);
|
|
}
|
|
}
|
|
|
|
function value(el) {
|
|
return el ? String(el.value || "").trim() : "";
|
|
}
|
|
|
|
function valueOf(root, selector) {
|
|
return value(root.querySelector(selector));
|
|
}
|
|
|
|
function clone(valueToClone) {
|
|
return JSON.parse(JSON.stringify(valueToClone));
|
|
}
|
|
|
|
function parseTemplateText(text) {
|
|
var trimmed = String(text || "").trim();
|
|
if (!trimmed) {
|
|
throw new Error("Datei ist leer.");
|
|
}
|
|
|
|
if (window.jsyaml && typeof window.jsyaml.load === "function") {
|
|
try {
|
|
var yaml = window.jsyaml.load(trimmed);
|
|
if (yaml && typeof yaml === "object") {
|
|
return yaml;
|
|
}
|
|
} catch (error) {
|
|
// Try JSON and the small fallback parser below.
|
|
}
|
|
}
|
|
|
|
if (trimmed.charAt(0) === "{" || trimmed.charAt(0) === "[") {
|
|
return JSON.parse(trimmed);
|
|
}
|
|
|
|
try {
|
|
return JSON.parse(trimmed);
|
|
} catch (error) {
|
|
return parseSimpleYaml(trimmed);
|
|
}
|
|
}
|
|
|
|
function normalizeTemplate(input, fallbackName) {
|
|
if (!input || !Array.isArray(input.steps)) {
|
|
throw new Error('Ungültige Vorlage: "steps" fehlt.');
|
|
}
|
|
|
|
return {
|
|
name: stringOr(input.name, fallbackName || "Template"),
|
|
module: stringOr(input.module, ""),
|
|
module_version: stringOr(input.module_version, ""),
|
|
olm_nummer: stringOr(input.olm_nummer || input.olm || input.olmNumber, ""),
|
|
pbx_version: stringOr(input.pbx_version, ""),
|
|
steps: input.steps.map(function (step, index) {
|
|
var kind = step.kind || step.type || "step";
|
|
if (kind === "group") {
|
|
return {
|
|
kind: "group",
|
|
title: stringOr(step.title || step.name, "Gruppe " + (index + 1)),
|
|
collapsed: !!step.collapsed
|
|
};
|
|
}
|
|
|
|
return {
|
|
kind: "step",
|
|
id: stringOr(step.id, "step-" + String(index + 1).padStart(3, "0")),
|
|
title: stringOr(step.title, ""),
|
|
expected: stringOr(step.expected, ""),
|
|
required: toBool(step.required),
|
|
status: stringOr(step.status, ""),
|
|
comment: stringOr(step.comment, ""),
|
|
evidence: stringOr(step.evidence, "")
|
|
};
|
|
})
|
|
};
|
|
}
|
|
|
|
function parseSimpleYaml(text) {
|
|
var lines = String(text || "").replace(/\r\n?/g, "\n").split("\n");
|
|
var out = {};
|
|
var steps = null;
|
|
var current = null;
|
|
var index = 0;
|
|
|
|
while (index < lines.length) {
|
|
var raw = lines[index];
|
|
var trimmed = raw.trim();
|
|
var indent = countIndent(raw);
|
|
|
|
if (!trimmed || trimmed.charAt(0) === "#") {
|
|
index++;
|
|
continue;
|
|
}
|
|
|
|
if (indent === 0) {
|
|
var top = splitKeyValue(trimmed);
|
|
if (!top) {
|
|
index++;
|
|
continue;
|
|
}
|
|
|
|
if (top.key === "steps") {
|
|
steps = [];
|
|
out.steps = steps;
|
|
current = null;
|
|
index++;
|
|
continue;
|
|
}
|
|
|
|
if (top.value === "|" || top.value === ">") {
|
|
var topBlock = readBlock(lines, index + 1, indent, top.value === ">");
|
|
out[top.key] = topBlock.value;
|
|
index = topBlock.next;
|
|
continue;
|
|
}
|
|
|
|
out[top.key] = parseScalar(top.value);
|
|
index++;
|
|
continue;
|
|
}
|
|
|
|
if (steps && indent >= 2) {
|
|
if (trimmed.indexOf("- ") === 0) {
|
|
current = {};
|
|
steps.push(current);
|
|
var rest = trimmed.slice(2).trim();
|
|
if (rest) {
|
|
var inline = splitKeyValue(rest);
|
|
if (inline) {
|
|
current[inline.key] = parseScalar(inline.value);
|
|
}
|
|
}
|
|
index++;
|
|
continue;
|
|
}
|
|
|
|
if (current) {
|
|
var pair = splitKeyValue(trimmed);
|
|
if (pair) {
|
|
if (pair.value === "|" || pair.value === ">") {
|
|
var block = readBlock(lines, index + 1, indent, pair.value === ">");
|
|
current[pair.key] = block.value;
|
|
index = block.next;
|
|
continue;
|
|
}
|
|
current[pair.key] = parseScalar(pair.value);
|
|
}
|
|
}
|
|
}
|
|
|
|
index++;
|
|
}
|
|
|
|
return out;
|
|
}
|
|
|
|
function countIndent(line) {
|
|
var match = String(line || "").match(/^ */);
|
|
return match ? match[0].length : 0;
|
|
}
|
|
|
|
function splitKeyValue(text) {
|
|
var position = text.indexOf(":");
|
|
if (position < 0) {
|
|
return null;
|
|
}
|
|
return {
|
|
key: text.slice(0, position).trim(),
|
|
value: text.slice(position + 1).trim()
|
|
};
|
|
}
|
|
|
|
function readBlock(lines, start, parentIndent, folded) {
|
|
var collected = [];
|
|
var index = start;
|
|
var minIndent = null;
|
|
|
|
while (index < lines.length) {
|
|
var raw = lines[index];
|
|
var trimmed = raw.trim();
|
|
var indent = countIndent(raw);
|
|
if (trimmed && indent <= parentIndent) {
|
|
break;
|
|
}
|
|
if (!trimmed) {
|
|
collected.push("");
|
|
index++;
|
|
continue;
|
|
}
|
|
if (minIndent === null || indent < minIndent) {
|
|
minIndent = indent;
|
|
}
|
|
collected.push(raw);
|
|
index++;
|
|
}
|
|
|
|
minIndent = minIndent || 0;
|
|
var value = collected.map(function (line) {
|
|
return line ? line.slice(minIndent) : "";
|
|
}).join("\n").trim();
|
|
|
|
if (folded) {
|
|
value = value.replace(/\n+/g, " ");
|
|
}
|
|
|
|
return { value: value, next: index };
|
|
}
|
|
|
|
function parseScalar(value) {
|
|
value = String(value || "").trim();
|
|
if (value === "") {
|
|
return "";
|
|
}
|
|
if (value === "true") {
|
|
return true;
|
|
}
|
|
if (value === "false") {
|
|
return false;
|
|
}
|
|
if (value === "null" || value === "~") {
|
|
return "";
|
|
}
|
|
if ((value.charAt(0) === '"' && value.charAt(value.length - 1) === '"') || (value.charAt(0) === "'" && value.charAt(value.length - 1) === "'")) {
|
|
value = value.slice(1, -1);
|
|
}
|
|
return value.replace(/\\"/g, '"').replace(/\\n/g, "\n");
|
|
}
|
|
|
|
function dumpTemplateYaml(template) {
|
|
if (window.jsyaml && typeof window.jsyaml.dump === "function") {
|
|
return window.jsyaml.dump(template, { lineWidth: 100 });
|
|
}
|
|
|
|
var lines = [
|
|
'name: "' + yamlString(template.name || "") + '"',
|
|
'module: "' + yamlString(template.module || "") + '"',
|
|
'module_version: "' + yamlString(template.module_version || "") + '"',
|
|
'olm_nummer: "' + yamlString(template.olm_nummer || "") + '"',
|
|
'pbx_version: "' + yamlString(template.pbx_version || "") + '"',
|
|
"steps:"
|
|
];
|
|
|
|
(template.steps || []).forEach(function (step) {
|
|
if (step.type === "group") {
|
|
lines.push(' - type: "group"');
|
|
lines.push(' title: "' + yamlString(step.title || "") + '"');
|
|
return;
|
|
}
|
|
lines.push(' - type: "step"');
|
|
lines.push(' id: "' + yamlString(step.id || "") + '"');
|
|
lines.push(' title: "' + yamlString(step.title || "") + '"');
|
|
lines.push(' expected: "' + yamlString(step.expected || "") + '"');
|
|
lines.push(" required: " + (step.required ? "true" : "false"));
|
|
});
|
|
|
|
return lines.join("\n") + "\n";
|
|
}
|
|
|
|
function yamlString(valueToEscape) {
|
|
return String(valueToEscape || "").replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
|
|
}
|
|
|
|
function toBool(valueToConvert) {
|
|
if (typeof valueToConvert === "boolean") {
|
|
return valueToConvert;
|
|
}
|
|
return ["1", "true", "yes", "ja"].indexOf(String(valueToConvert || "").toLowerCase()) !== -1;
|
|
}
|
|
|
|
function stringOr(valueToConvert, fallback) {
|
|
if (valueToConvert === undefined || valueToConvert === null) {
|
|
return fallback;
|
|
}
|
|
return String(valueToConvert);
|
|
}
|
|
|
|
function loadScript(url) {
|
|
return new Promise(function (resolve, reject) {
|
|
var script = document.createElement("script");
|
|
script.src = url;
|
|
script.async = true;
|
|
script.onload = resolve;
|
|
script.onerror = reject;
|
|
document.head.appendChild(script);
|
|
});
|
|
}
|
|
|
|
function imageToDataUrl(src) {
|
|
return new Promise(function (resolve, reject) {
|
|
if (!src) {
|
|
reject(new Error("No image source."));
|
|
return;
|
|
}
|
|
|
|
var image = new Image();
|
|
image.crossOrigin = "anonymous";
|
|
image.onload = function () {
|
|
try {
|
|
var canvas = document.createElement("canvas");
|
|
canvas.width = image.naturalWidth || image.width;
|
|
canvas.height = image.naturalHeight || image.height;
|
|
var context = canvas.getContext("2d");
|
|
context.drawImage(image, 0, 0);
|
|
resolve(canvas.toDataURL("image/png"));
|
|
} catch (error) {
|
|
reject(error);
|
|
}
|
|
};
|
|
image.onerror = reject;
|
|
image.src = src;
|
|
});
|
|
}
|
|
|
|
function replaceSymbols(valueToClean) {
|
|
return String(valueToClean === undefined || valueToClean === null ? "" : valueToClean)
|
|
.replace(/[\u2192\u21A6\u21E8\u27A1\u2794\u27F6\u27F7\u27F9\u279D\u279E\u279F\u27A0]/g, "->")
|
|
.replace(/[\u2190\u21A4\u21E6\u2B05\u27F5]/g, "<-")
|
|
.replace(/[\u2194\u21D4\u27F7\u27F8\u2B04]/g, "<->")
|
|
.replace(/[\u2013\u2014\u2011]/g, "-")
|
|
.replace(/\u2026/g, "...")
|
|
.replace(/\u00D7/g, "x");
|
|
}
|
|
|
|
function toPdfSafe(valueToClean) {
|
|
return String(valueToClean || "").replace(/[^\x09\x0A\x0D\x20-\x7E\u00A0-\u00FF]/g, "?");
|
|
}
|
|
|
|
function sanitizePdfText(valueToClean) {
|
|
return toPdfSafe(replaceSymbols(String(valueToClean === undefined || valueToClean === null ? "" : valueToClean)
|
|
.replace(/\u00A0/g, " ")
|
|
.replace(/[\u201C\u201D]/g, '"')
|
|
.replace(/[\u2018\u2019]/g, "'")
|
|
.replace(/\s+/g, " ")
|
|
.trim()));
|
|
}
|
|
|
|
function download(content, filename, type) {
|
|
var blob = content instanceof Blob ? content : new Blob([content], { type: type });
|
|
var url = URL.createObjectURL(blob);
|
|
var link = document.createElement("a");
|
|
link.href = url;
|
|
link.download = filename;
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
link.remove();
|
|
setTimeout(function () {
|
|
URL.revokeObjectURL(url);
|
|
}, 1500);
|
|
}
|
|
|
|
function runBaseName(prefix, run) {
|
|
return [
|
|
prefix,
|
|
safeName(run.module || "modul"),
|
|
safeName(run.module_version || "version"),
|
|
safeName(run.pbx_version || "pbx")
|
|
].join("-");
|
|
}
|
|
|
|
function safeName(valueToClean) {
|
|
return String(valueToClean || "")
|
|
.trim()
|
|
.toLowerCase()
|
|
.replace(/\s+/g, "-")
|
|
.replace(/[^\w.-]/g, "") || "qa";
|
|
}
|
|
|
|
function sameModuleName(left, right) {
|
|
return String(left || "").trim() === String(right || "").trim();
|
|
}
|
|
|
|
function mdCell(valueToEscape) {
|
|
return String(valueToEscape || "").replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
}
|
|
|
|
function csvCell(valueToEscape) {
|
|
return '"' + String(valueToEscape === undefined || valueToEscape === null ? "" : valueToEscape).replace(/"/g, '""') + '"';
|
|
}
|
|
|
|
function countStatuses(steps) {
|
|
var counts = { total: 0, pass: 0, fail: 0, skip: 0, blocked: 0, missingRequired: 0 };
|
|
(steps || []).forEach(function (step) {
|
|
if ((step.kind || step.type || "step") !== "step") {
|
|
return;
|
|
}
|
|
counts.total++;
|
|
if (Object.prototype.hasOwnProperty.call(counts, step.status)) {
|
|
counts[step.status]++;
|
|
}
|
|
if (step.required && !step.status) {
|
|
counts.missingRequired++;
|
|
}
|
|
});
|
|
return counts;
|
|
}
|
|
|
|
function escHtml(valueToEscape) {
|
|
return String(valueToEscape === undefined || valueToEscape === null ? "" : valueToEscape)
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">");
|
|
}
|
|
|
|
function escAttr(valueToEscape) {
|
|
return escHtml(valueToEscape).replace(/"/g, """);
|
|
}
|
|
|
|
})();
|