@msalaam/xray-qe-toolkit 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +16 -0
- package/README.md +893 -0
- package/bin/cli.js +137 -0
- package/commands/createExecution.js +46 -0
- package/commands/editJson.js +165 -0
- package/commands/genPipeline.js +42 -0
- package/commands/genPostman.js +70 -0
- package/commands/genTests.js +138 -0
- package/commands/importResults.js +50 -0
- package/commands/init.js +141 -0
- package/commands/pushTests.js +114 -0
- package/lib/config.js +108 -0
- package/lib/index.js +31 -0
- package/lib/logger.js +43 -0
- package/lib/postmanGenerator.js +305 -0
- package/lib/testCaseBuilder.js +202 -0
- package/lib/xrayClient.js +416 -0
- package/package.json +61 -0
- package/schema/tests.schema.json +112 -0
- package/templates/README.template.md +161 -0
- package/templates/azure-pipelines.yml +65 -0
- package/templates/knowledge-README.md +121 -0
- package/templates/tests.json +72 -0
- package/templates/xray-mapping.json +1 -0
- package/ui/editor/editor.js +484 -0
- package/ui/editor/index.html +150 -0
- package/ui/editor/styles.css +550 -0
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XQT — QE Review Gate Editor (Client-Side)
|
|
3
|
+
*
|
|
4
|
+
* Pure vanilla JS — no frameworks, no build step.
|
|
5
|
+
* Communicates with the Express server via REST API.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ─── State ──────────────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
let testsConfig = { testExecution: { summary: "", description: "" }, tests: [] };
|
|
11
|
+
let mapping = {};
|
|
12
|
+
let selectedIndex = -1;
|
|
13
|
+
let isDirty = false;
|
|
14
|
+
|
|
15
|
+
// ─── DOM refs ───────────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
const $testList = document.getElementById("test-list");
|
|
18
|
+
const $testCount = document.getElementById("test-count");
|
|
19
|
+
const $statusBadge = document.getElementById("status-badge");
|
|
20
|
+
const $placeholder = document.getElementById("placeholder-msg");
|
|
21
|
+
const $editor = document.getElementById("test-editor");
|
|
22
|
+
const $searchInput = document.getElementById("search-input");
|
|
23
|
+
const $filterTag = document.getElementById("filter-tag");
|
|
24
|
+
const $stepsContainer = document.getElementById("steps-container");
|
|
25
|
+
const $toast = document.getElementById("toast");
|
|
26
|
+
|
|
27
|
+
// Exec fields
|
|
28
|
+
const $execSummary = document.getElementById("exec-summary");
|
|
29
|
+
const $execDescription = document.getElementById("exec-description");
|
|
30
|
+
|
|
31
|
+
// Test fields
|
|
32
|
+
const $testId = document.getElementById("test-id");
|
|
33
|
+
const $testIdError = document.getElementById("test-id-error");
|
|
34
|
+
const $testSummary = document.getElementById("test-summary");
|
|
35
|
+
const $testDescription = document.getElementById("test-description");
|
|
36
|
+
const $testPriority = document.getElementById("test-priority");
|
|
37
|
+
const $testLabels = document.getElementById("test-labels");
|
|
38
|
+
const $testSkip = document.getElementById("test-skip");
|
|
39
|
+
const $mappingInfo = document.getElementById("mapping-info");
|
|
40
|
+
const $mappingKey = document.getElementById("mapping-key");
|
|
41
|
+
const $mappingId = document.getElementById("mapping-id");
|
|
42
|
+
|
|
43
|
+
// ─── Init ───────────────────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
document.addEventListener("DOMContentLoaded", async () => {
|
|
46
|
+
await loadData();
|
|
47
|
+
bindEvents();
|
|
48
|
+
renderTestList();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
async function loadData() {
|
|
52
|
+
try {
|
|
53
|
+
const [testsRes, mappingRes] = await Promise.all([
|
|
54
|
+
fetch("/api/tests").then((r) => r.json()),
|
|
55
|
+
fetch("/api/mapping").then((r) => r.json()),
|
|
56
|
+
]);
|
|
57
|
+
testsConfig = testsRes;
|
|
58
|
+
mapping = mappingRes;
|
|
59
|
+
|
|
60
|
+
// Ensure structure
|
|
61
|
+
if (!testsConfig.testExecution) {
|
|
62
|
+
testsConfig.testExecution = { summary: "", description: "" };
|
|
63
|
+
}
|
|
64
|
+
if (!testsConfig.tests) testsConfig.tests = [];
|
|
65
|
+
|
|
66
|
+
// Populate exec fields
|
|
67
|
+
$execSummary.value = testsConfig.testExecution.summary || "";
|
|
68
|
+
$execDescription.value = testsConfig.testExecution.description || "";
|
|
69
|
+
|
|
70
|
+
showToast("Tests loaded", "success");
|
|
71
|
+
} catch (err) {
|
|
72
|
+
showToast(`Failed to load: ${err.message}`, "error");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ─── Event binding ──────────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
function bindEvents() {
|
|
79
|
+
document.getElementById("btn-add-test").addEventListener("click", addNewTest);
|
|
80
|
+
document.getElementById("btn-save").addEventListener("click", saveAndExit);
|
|
81
|
+
document.getElementById("btn-delete-test").addEventListener("click", deleteCurrentTest);
|
|
82
|
+
document.getElementById("btn-add-step").addEventListener("click", addNewStep);
|
|
83
|
+
|
|
84
|
+
$searchInput.addEventListener("input", renderTestList);
|
|
85
|
+
$filterTag.addEventListener("change", renderTestList);
|
|
86
|
+
|
|
87
|
+
// Auto-save fields on change (to state, not disk)
|
|
88
|
+
const fieldInputs = [$testId, $testSummary, $testDescription, $testPriority, $testLabels, $testSkip];
|
|
89
|
+
fieldInputs.forEach((el) => {
|
|
90
|
+
el.addEventListener("input", syncEditorToState);
|
|
91
|
+
el.addEventListener("change", syncEditorToState);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Exec fields
|
|
95
|
+
$execSummary.addEventListener("input", () => {
|
|
96
|
+
testsConfig.testExecution.summary = $execSummary.value;
|
|
97
|
+
markDirty();
|
|
98
|
+
});
|
|
99
|
+
$execDescription.addEventListener("input", () => {
|
|
100
|
+
testsConfig.testExecution.description = $execDescription.value;
|
|
101
|
+
markDirty();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Tag checkboxes
|
|
105
|
+
document.querySelectorAll("#tag-selector input[type=checkbox]").forEach((cb) => {
|
|
106
|
+
cb.addEventListener("change", syncEditorToState);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Warn about unsaved changes
|
|
110
|
+
window.addEventListener("beforeunload", (e) => {
|
|
111
|
+
if (isDirty) {
|
|
112
|
+
e.preventDefault();
|
|
113
|
+
e.returnValue = "You have unsaved changes.";
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ─── Test List Rendering ────────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
function renderTestList() {
|
|
121
|
+
const search = $searchInput.value.toLowerCase();
|
|
122
|
+
const tagFilter = $filterTag.value;
|
|
123
|
+
|
|
124
|
+
$testList.innerHTML = "";
|
|
125
|
+
|
|
126
|
+
testsConfig.tests.forEach((test, idx) => {
|
|
127
|
+
// Filter by search
|
|
128
|
+
const searchable = `${test.test_id} ${test.xray.summary} ${test.xray.description || ""}`.toLowerCase();
|
|
129
|
+
if (search && !searchable.includes(search)) return;
|
|
130
|
+
|
|
131
|
+
// Filter by tag
|
|
132
|
+
if (tagFilter && !(test.tags || []).includes(tagFilter)) return;
|
|
133
|
+
|
|
134
|
+
const li = document.createElement("li");
|
|
135
|
+
li.className = idx === selectedIndex ? "active" : "";
|
|
136
|
+
li.addEventListener("click", () => selectTest(idx));
|
|
137
|
+
|
|
138
|
+
// Test ID
|
|
139
|
+
const idSpan = document.createElement("span");
|
|
140
|
+
idSpan.className = "test-list-id";
|
|
141
|
+
idSpan.textContent = test.test_id;
|
|
142
|
+
li.appendChild(idSpan);
|
|
143
|
+
|
|
144
|
+
// Summary
|
|
145
|
+
const summarySpan = document.createElement("span");
|
|
146
|
+
summarySpan.className = "test-list-summary";
|
|
147
|
+
summarySpan.textContent = test.xray.summary;
|
|
148
|
+
li.appendChild(summarySpan);
|
|
149
|
+
|
|
150
|
+
// Meta row
|
|
151
|
+
const metaDiv = document.createElement("div");
|
|
152
|
+
metaDiv.className = "test-list-meta";
|
|
153
|
+
|
|
154
|
+
// Tags
|
|
155
|
+
(test.tags || []).slice(0, 3).forEach((tag) => {
|
|
156
|
+
const tagSpan = document.createElement("span");
|
|
157
|
+
tagSpan.className = `test-list-tag tag-${tag}`;
|
|
158
|
+
tagSpan.textContent = tag;
|
|
159
|
+
metaDiv.appendChild(tagSpan);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// Skip indicator
|
|
163
|
+
if (test.skip) {
|
|
164
|
+
const skipSpan = document.createElement("span");
|
|
165
|
+
skipSpan.className = "test-list-skip";
|
|
166
|
+
skipSpan.textContent = "(skipped)";
|
|
167
|
+
metaDiv.appendChild(skipSpan);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Xray mapping indicator
|
|
171
|
+
const m = mapping[test.test_id];
|
|
172
|
+
if (m && m.key) {
|
|
173
|
+
const xraySpan = document.createElement("span");
|
|
174
|
+
xraySpan.className = "test-list-xray";
|
|
175
|
+
xraySpan.textContent = m.key;
|
|
176
|
+
metaDiv.appendChild(xraySpan);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
li.appendChild(metaDiv);
|
|
180
|
+
$testList.appendChild(li);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// Update count
|
|
184
|
+
$testCount.textContent = `${testsConfig.tests.length} test${testsConfig.tests.length !== 1 ? "s" : ""}`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ─── Test Selection ─────────────────────────────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
function selectTest(idx) {
|
|
190
|
+
selectedIndex = idx;
|
|
191
|
+
const test = testsConfig.tests[idx];
|
|
192
|
+
if (!test) return;
|
|
193
|
+
|
|
194
|
+
$placeholder.classList.add("hidden");
|
|
195
|
+
$editor.classList.remove("hidden");
|
|
196
|
+
|
|
197
|
+
// Populate fields
|
|
198
|
+
$testId.value = test.test_id || "";
|
|
199
|
+
$testSummary.value = test.xray.summary || "";
|
|
200
|
+
$testDescription.value = test.xray.description || "";
|
|
201
|
+
$testPriority.value = test.xray.priority || "";
|
|
202
|
+
$testLabels.value = (test.xray.labels || []).join(", ");
|
|
203
|
+
$testSkip.checked = !!test.skip;
|
|
204
|
+
|
|
205
|
+
// Update title
|
|
206
|
+
document.getElementById("editor-title").textContent = test.test_id || "New Test";
|
|
207
|
+
|
|
208
|
+
// Tags
|
|
209
|
+
const tags = test.tags || [];
|
|
210
|
+
document.querySelectorAll("#tag-selector input[type=checkbox]").forEach((cb) => {
|
|
211
|
+
cb.checked = tags.includes(cb.value);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// Mapping info
|
|
215
|
+
const m = mapping[test.test_id];
|
|
216
|
+
if (m && m.key) {
|
|
217
|
+
$mappingInfo.classList.remove("hidden");
|
|
218
|
+
$mappingKey.textContent = m.key;
|
|
219
|
+
$mappingId.textContent = `(ID: ${m.id})`;
|
|
220
|
+
} else {
|
|
221
|
+
$mappingInfo.classList.add("hidden");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Clear ID error
|
|
225
|
+
$testIdError.classList.add("hidden");
|
|
226
|
+
$testId.classList.remove("error");
|
|
227
|
+
|
|
228
|
+
// Render steps
|
|
229
|
+
renderSteps(test.xray.steps || []);
|
|
230
|
+
|
|
231
|
+
// Re-render list to update active state
|
|
232
|
+
renderTestList();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ─── Steps Rendering ────────────────────────────────────────────────────────────
|
|
236
|
+
|
|
237
|
+
function renderSteps(steps) {
|
|
238
|
+
$stepsContainer.innerHTML = "";
|
|
239
|
+
|
|
240
|
+
steps.forEach((step, idx) => {
|
|
241
|
+
const card = document.createElement("div");
|
|
242
|
+
card.className = "step-card";
|
|
243
|
+
card.innerHTML = `
|
|
244
|
+
<div class="step-card-header">
|
|
245
|
+
<span class="step-number">Step ${idx + 1}</span>
|
|
246
|
+
<button class="btn-remove" data-idx="${idx}" title="Remove step">✕</button>
|
|
247
|
+
</div>
|
|
248
|
+
<div class="form-row">
|
|
249
|
+
<label>Action</label>
|
|
250
|
+
<input type="text" class="step-action" data-idx="${idx}" value="${escapeHtml(step.action || "")}" placeholder="What action to perform..." />
|
|
251
|
+
</div>
|
|
252
|
+
<div class="form-row">
|
|
253
|
+
<label>Data</label>
|
|
254
|
+
<input type="text" class="step-data" data-idx="${idx}" value="${escapeHtml(step.data || "")}" placeholder="Input data or parameters..." />
|
|
255
|
+
</div>
|
|
256
|
+
<div class="form-row">
|
|
257
|
+
<label>Expected Result</label>
|
|
258
|
+
<input type="text" class="step-expected" data-idx="${idx}" value="${escapeHtml(step.expected_result || "")}" placeholder="Expected outcome..." />
|
|
259
|
+
</div>
|
|
260
|
+
`;
|
|
261
|
+
$stepsContainer.appendChild(card);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// Bind step events
|
|
265
|
+
$stepsContainer.querySelectorAll(".btn-remove").forEach((btn) => {
|
|
266
|
+
btn.addEventListener("click", (e) => {
|
|
267
|
+
const idx = parseInt(e.target.dataset.idx);
|
|
268
|
+
removeStep(idx);
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
$stepsContainer.querySelectorAll(".step-action, .step-data, .step-expected").forEach((input) => {
|
|
273
|
+
input.addEventListener("input", syncStepsToState);
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ─── Sync Editor → State ────────────────────────────────────────────────────────
|
|
278
|
+
|
|
279
|
+
function syncEditorToState() {
|
|
280
|
+
if (selectedIndex < 0) return;
|
|
281
|
+
const test = testsConfig.tests[selectedIndex];
|
|
282
|
+
if (!test) return;
|
|
283
|
+
|
|
284
|
+
// Validate test_id uniqueness
|
|
285
|
+
const newId = $testId.value.trim();
|
|
286
|
+
const duplicate = testsConfig.tests.some((t, i) => i !== selectedIndex && t.test_id === newId);
|
|
287
|
+
if (duplicate) {
|
|
288
|
+
$testIdError.textContent = "Duplicate test_id";
|
|
289
|
+
$testIdError.classList.remove("hidden");
|
|
290
|
+
$testId.classList.add("error");
|
|
291
|
+
} else {
|
|
292
|
+
$testIdError.classList.add("hidden");
|
|
293
|
+
$testId.classList.remove("error");
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
test.test_id = newId;
|
|
297
|
+
test.xray.summary = $testSummary.value;
|
|
298
|
+
test.xray.description = $testDescription.value;
|
|
299
|
+
test.xray.priority = $testPriority.value || undefined;
|
|
300
|
+
test.xray.labels = $testLabels.value
|
|
301
|
+
.split(",")
|
|
302
|
+
.map((s) => s.trim())
|
|
303
|
+
.filter(Boolean);
|
|
304
|
+
test.skip = $testSkip.checked || undefined;
|
|
305
|
+
|
|
306
|
+
// Tags
|
|
307
|
+
const tags = [];
|
|
308
|
+
document.querySelectorAll("#tag-selector input[type=checkbox]:checked").forEach((cb) => {
|
|
309
|
+
tags.push(cb.value);
|
|
310
|
+
});
|
|
311
|
+
test.tags = tags.length > 0 ? tags : undefined;
|
|
312
|
+
|
|
313
|
+
// Update editor title
|
|
314
|
+
document.getElementById("editor-title").textContent = test.test_id || "New Test";
|
|
315
|
+
|
|
316
|
+
markDirty();
|
|
317
|
+
renderTestList();
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function syncStepsToState() {
|
|
321
|
+
if (selectedIndex < 0) return;
|
|
322
|
+
const test = testsConfig.tests[selectedIndex];
|
|
323
|
+
if (!test) return;
|
|
324
|
+
|
|
325
|
+
const steps = [];
|
|
326
|
+
const cards = $stepsContainer.querySelectorAll(".step-card");
|
|
327
|
+
cards.forEach((card) => {
|
|
328
|
+
steps.push({
|
|
329
|
+
action: card.querySelector(".step-action").value,
|
|
330
|
+
data: card.querySelector(".step-data").value,
|
|
331
|
+
expected_result: card.querySelector(".step-expected").value,
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
test.xray.steps = steps;
|
|
336
|
+
markDirty();
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// ─── CRUD actions ───────────────────────────────────────────────────────────────
|
|
340
|
+
|
|
341
|
+
function addNewTest() {
|
|
342
|
+
const newTest = {
|
|
343
|
+
test_id: `TC-NEW-${String(testsConfig.tests.length + 1).padStart(3, "0")}`,
|
|
344
|
+
tags: ["regression"],
|
|
345
|
+
xray: {
|
|
346
|
+
summary: "New test case — edit me",
|
|
347
|
+
description: "",
|
|
348
|
+
priority: "Medium",
|
|
349
|
+
labels: [],
|
|
350
|
+
steps: [
|
|
351
|
+
{ action: "", data: "", expected_result: "" },
|
|
352
|
+
],
|
|
353
|
+
},
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
testsConfig.tests.push(newTest);
|
|
357
|
+
markDirty();
|
|
358
|
+
renderTestList();
|
|
359
|
+
selectTest(testsConfig.tests.length - 1);
|
|
360
|
+
showToast("New test added", "info");
|
|
361
|
+
|
|
362
|
+
// Focus on the test_id field
|
|
363
|
+
setTimeout(() => {
|
|
364
|
+
$testId.focus();
|
|
365
|
+
$testId.select();
|
|
366
|
+
}, 100);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function deleteCurrentTest() {
|
|
370
|
+
if (selectedIndex < 0) return;
|
|
371
|
+
|
|
372
|
+
const test = testsConfig.tests[selectedIndex];
|
|
373
|
+
if (!confirm(`Delete test "${test.test_id}"?`)) return;
|
|
374
|
+
|
|
375
|
+
testsConfig.tests.splice(selectedIndex, 1);
|
|
376
|
+
selectedIndex = -1;
|
|
377
|
+
markDirty();
|
|
378
|
+
renderTestList();
|
|
379
|
+
|
|
380
|
+
$editor.classList.add("hidden");
|
|
381
|
+
$placeholder.classList.remove("hidden");
|
|
382
|
+
showToast("Test deleted", "info");
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function addNewStep() {
|
|
386
|
+
if (selectedIndex < 0) return;
|
|
387
|
+
const test = testsConfig.tests[selectedIndex];
|
|
388
|
+
if (!test.xray.steps) test.xray.steps = [];
|
|
389
|
+
|
|
390
|
+
test.xray.steps.push({ action: "", data: "", expected_result: "" });
|
|
391
|
+
markDirty();
|
|
392
|
+
renderSteps(test.xray.steps);
|
|
393
|
+
|
|
394
|
+
// Focus the new step's action field
|
|
395
|
+
const lastAction = $stepsContainer.querySelector(".step-card:last-child .step-action");
|
|
396
|
+
if (lastAction) setTimeout(() => lastAction.focus(), 100);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function removeStep(idx) {
|
|
400
|
+
if (selectedIndex < 0) return;
|
|
401
|
+
const test = testsConfig.tests[selectedIndex];
|
|
402
|
+
test.xray.steps.splice(idx, 1);
|
|
403
|
+
markDirty();
|
|
404
|
+
renderSteps(test.xray.steps);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// ─── Save & Exit ────────────────────────────────────────────────────────────────
|
|
408
|
+
|
|
409
|
+
async function saveAndExit() {
|
|
410
|
+
// Sync exec fields
|
|
411
|
+
testsConfig.testExecution = {
|
|
412
|
+
summary: $execSummary.value,
|
|
413
|
+
description: $execDescription.value,
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
// Clean up undefined/empty fields
|
|
417
|
+
testsConfig.tests.forEach((test) => {
|
|
418
|
+
if (!test.skip) delete test.skip;
|
|
419
|
+
if (!test.tags || test.tags.length === 0) delete test.tags;
|
|
420
|
+
if (!test.xray.priority) delete test.xray.priority;
|
|
421
|
+
if (!test.xray.labels || test.xray.labels.length === 0) delete test.xray.labels;
|
|
422
|
+
if (!test.xray.description) delete test.xray.description;
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
try {
|
|
426
|
+
const res = await fetch("/api/tests", {
|
|
427
|
+
method: "PUT",
|
|
428
|
+
headers: { "Content-Type": "application/json" },
|
|
429
|
+
body: JSON.stringify(testsConfig),
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
const data = await res.json();
|
|
433
|
+
|
|
434
|
+
if (!res.ok) {
|
|
435
|
+
showToast(`Save failed: ${data.error}`, "error");
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
isDirty = false;
|
|
440
|
+
$statusBadge.textContent = "Saved";
|
|
441
|
+
$statusBadge.className = "badge badge-loaded";
|
|
442
|
+
showToast(`Saved ${data.count} test(s) — shutting down...`, "success");
|
|
443
|
+
|
|
444
|
+
// Trigger server shutdown after a brief delay
|
|
445
|
+
setTimeout(async () => {
|
|
446
|
+
try {
|
|
447
|
+
await fetch("/api/shutdown", { method: "POST" });
|
|
448
|
+
} catch {
|
|
449
|
+
// Expected — server shutting down
|
|
450
|
+
}
|
|
451
|
+
document.body.innerHTML = `
|
|
452
|
+
<div style="display:flex;align-items:center;justify-content:center;height:100vh;background:#0d1117;color:#e6edf3;font-family:sans-serif;">
|
|
453
|
+
<div style="text-align:center;">
|
|
454
|
+
<div style="font-size:48px;margin-bottom:16px;">✅</div>
|
|
455
|
+
<h2>Tests Saved</h2>
|
|
456
|
+
<p style="color:#8b949e;margin-top:8px;">You can close this tab. Run <code style="color:#58a6ff;">npx xray-qe push-tests</code> to push to Xray.</p>
|
|
457
|
+
</div>
|
|
458
|
+
</div>
|
|
459
|
+
`;
|
|
460
|
+
}, 1000);
|
|
461
|
+
} catch (err) {
|
|
462
|
+
showToast(`Save error: ${err.message}`, "error");
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────────
|
|
467
|
+
|
|
468
|
+
function markDirty() {
|
|
469
|
+
isDirty = true;
|
|
470
|
+
$statusBadge.textContent = "Unsaved";
|
|
471
|
+
$statusBadge.className = "badge badge-unsaved";
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function showToast(message, type = "info") {
|
|
475
|
+
$toast.textContent = message;
|
|
476
|
+
$toast.className = `toast toast-${type}`;
|
|
477
|
+
setTimeout(() => $toast.classList.add("hidden"), 3000);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function escapeHtml(str) {
|
|
481
|
+
const div = document.createElement("div");
|
|
482
|
+
div.textContent = str;
|
|
483
|
+
return div.innerHTML.replace(/"/g, """);
|
|
484
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>XQT — QE Review Gate</title>
|
|
7
|
+
<link rel="stylesheet" href="styles.css">
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<header>
|
|
11
|
+
<div class="header-left">
|
|
12
|
+
<h1>🧪 XQT — QE Review Gate</h1>
|
|
13
|
+
<span class="subtitle">@msalaam/xray-qe-toolkit</span>
|
|
14
|
+
</div>
|
|
15
|
+
<div class="header-right">
|
|
16
|
+
<span id="status-badge" class="badge badge-loaded">Loaded</span>
|
|
17
|
+
<span id="test-count" class="test-count">0 tests</span>
|
|
18
|
+
<button id="btn-add-test" class="btn btn-primary" title="Add new test">+ Add Test</button>
|
|
19
|
+
<button id="btn-save" class="btn btn-success" title="Save and exit editor">💾 Save & Exit</button>
|
|
20
|
+
</div>
|
|
21
|
+
</header>
|
|
22
|
+
|
|
23
|
+
<div class="container">
|
|
24
|
+
<!-- ── Left Panel: Test List ──────────────────────────────────────────── -->
|
|
25
|
+
<aside id="test-list-panel">
|
|
26
|
+
<div class="panel-header">
|
|
27
|
+
<input type="text" id="search-input" placeholder="Search tests..." />
|
|
28
|
+
<select id="filter-tag">
|
|
29
|
+
<option value="">All tags</option>
|
|
30
|
+
<option value="regression">regression</option>
|
|
31
|
+
<option value="smoke">smoke</option>
|
|
32
|
+
<option value="edge">edge</option>
|
|
33
|
+
<option value="critical">critical</option>
|
|
34
|
+
<option value="integration">integration</option>
|
|
35
|
+
<option value="e2e">e2e</option>
|
|
36
|
+
<option value="security">security</option>
|
|
37
|
+
<option value="performance">performance</option>
|
|
38
|
+
</select>
|
|
39
|
+
</div>
|
|
40
|
+
<ul id="test-list"></ul>
|
|
41
|
+
</aside>
|
|
42
|
+
|
|
43
|
+
<!-- ── Right Panel: Test Detail Editor ─────────────────────────────── -->
|
|
44
|
+
<main id="editor-panel">
|
|
45
|
+
<div id="placeholder-msg" class="placeholder">
|
|
46
|
+
<div class="placeholder-icon">📋</div>
|
|
47
|
+
<h2>Select a test to edit</h2>
|
|
48
|
+
<p>Choose a test from the left panel, or click <strong>+ Add Test</strong> to create a new one.</p>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<div id="test-editor" class="hidden">
|
|
52
|
+
<!-- Test Execution Config (shown at top when editing) -->
|
|
53
|
+
<section id="exec-section" class="editor-section">
|
|
54
|
+
<h3>📌 Test Execution</h3>
|
|
55
|
+
<div class="form-row">
|
|
56
|
+
<label for="exec-summary">Summary</label>
|
|
57
|
+
<input type="text" id="exec-summary" placeholder="Sprint XX - Automated Regression Suite" />
|
|
58
|
+
</div>
|
|
59
|
+
<div class="form-row">
|
|
60
|
+
<label for="exec-description">Description</label>
|
|
61
|
+
<textarea id="exec-description" rows="2" placeholder="Test execution description..."></textarea>
|
|
62
|
+
</div>
|
|
63
|
+
</section>
|
|
64
|
+
|
|
65
|
+
<!-- Test Fields -->
|
|
66
|
+
<section class="editor-section">
|
|
67
|
+
<div class="section-header">
|
|
68
|
+
<h3 id="editor-title">Test Details</h3>
|
|
69
|
+
<div class="section-actions">
|
|
70
|
+
<label class="toggle-label">
|
|
71
|
+
<input type="checkbox" id="test-skip" /> Skip this test
|
|
72
|
+
</label>
|
|
73
|
+
<button id="btn-delete-test" class="btn btn-danger btn-sm" title="Delete this test">🗑 Delete</button>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<div class="form-row">
|
|
78
|
+
<label for="test-id">Test ID</label>
|
|
79
|
+
<input type="text" id="test-id" placeholder="TC-API-GET-001" />
|
|
80
|
+
<span id="test-id-error" class="field-error hidden"></span>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<div class="form-row">
|
|
84
|
+
<label for="test-summary">Summary</label>
|
|
85
|
+
<input type="text" id="test-summary" placeholder="Verify GET /api/resource returns 200" />
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<div class="form-row">
|
|
89
|
+
<label for="test-description">Description</label>
|
|
90
|
+
<textarea id="test-description" rows="3" placeholder="Test description..."></textarea>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<div class="form-row form-row-inline">
|
|
94
|
+
<div>
|
|
95
|
+
<label for="test-priority">Priority</label>
|
|
96
|
+
<select id="test-priority">
|
|
97
|
+
<option value="">— Select —</option>
|
|
98
|
+
<option value="Highest">Highest</option>
|
|
99
|
+
<option value="High">High</option>
|
|
100
|
+
<option value="Medium">Medium</option>
|
|
101
|
+
<option value="Low">Low</option>
|
|
102
|
+
<option value="Lowest">Lowest</option>
|
|
103
|
+
</select>
|
|
104
|
+
</div>
|
|
105
|
+
<div>
|
|
106
|
+
<label for="test-labels">Labels <small>(comma-separated)</small></label>
|
|
107
|
+
<input type="text" id="test-labels" placeholder="API, GET, Regression" />
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
<div class="form-row">
|
|
112
|
+
<label>Tags</label>
|
|
113
|
+
<div id="tag-selector" class="tag-selector">
|
|
114
|
+
<label class="tag-chip"><input type="checkbox" value="regression" /> regression</label>
|
|
115
|
+
<label class="tag-chip"><input type="checkbox" value="smoke" /> smoke</label>
|
|
116
|
+
<label class="tag-chip"><input type="checkbox" value="edge" /> edge</label>
|
|
117
|
+
<label class="tag-chip"><input type="checkbox" value="critical" /> critical</label>
|
|
118
|
+
<label class="tag-chip"><input type="checkbox" value="integration" /> integration</label>
|
|
119
|
+
<label class="tag-chip"><input type="checkbox" value="e2e" /> e2e</label>
|
|
120
|
+
<label class="tag-chip"><input type="checkbox" value="security" /> security</label>
|
|
121
|
+
<label class="tag-chip"><input type="checkbox" value="performance" /> performance</label>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
<!-- Xray Mapping Info (read-only) -->
|
|
126
|
+
<div id="mapping-info" class="mapping-info hidden">
|
|
127
|
+
<span class="mapping-label">Xray:</span>
|
|
128
|
+
<span id="mapping-key" class="mapping-key"></span>
|
|
129
|
+
<span id="mapping-id" class="mapping-id"></span>
|
|
130
|
+
</div>
|
|
131
|
+
</section>
|
|
132
|
+
|
|
133
|
+
<!-- Steps Editor -->
|
|
134
|
+
<section class="editor-section">
|
|
135
|
+
<div class="section-header">
|
|
136
|
+
<h3>🔢 Test Steps</h3>
|
|
137
|
+
<button id="btn-add-step" class="btn btn-primary btn-sm">+ Add Step</button>
|
|
138
|
+
</div>
|
|
139
|
+
<div id="steps-container"></div>
|
|
140
|
+
</section>
|
|
141
|
+
</div>
|
|
142
|
+
</main>
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
<!-- Toast Notification -->
|
|
146
|
+
<div id="toast" class="toast hidden"></div>
|
|
147
|
+
|
|
148
|
+
<script src="editor.js"></script>
|
|
149
|
+
</body>
|
|
150
|
+
</html>
|