arreglos de grok
This commit is contained in:
@@ -0,0 +1,731 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Test Case Editor • PBX Automation</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0b1120;
|
||||
--card: #1e2937;
|
||||
--border: #334155;
|
||||
}
|
||||
body { background: var(--bg); color: #e2e8f0; }
|
||||
.card { background: var(--card); border: 1px solid var(--border); }
|
||||
.row-hover:hover { background: #162032; }
|
||||
.section-title { font-size: 11px; font-weight: 700; letter-spacing: .6px; text-transform: uppercase; color: #64748b; }
|
||||
.modal { animation: fadeInScale .15s ease-out; }
|
||||
@keyframes fadeInScale { from { opacity: 0; transform: scale(0.96); } to { opacity: 1; transform: scale(1); } }
|
||||
.editable-list li { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
|
||||
.editable-list input { flex: 1; background: #0f172a; border: 1px solid #334155; color: #e2e8f0; padding: 6px 10px; border-radius: 6px; font-size: 13px; }
|
||||
.editable-list button { color: #64748b; }
|
||||
.editable-list button:hover { color: #f87171; }
|
||||
.dep-row { display: flex; gap: 8px; margin-bottom: 6px; }
|
||||
.dep-row input { background: #0f172a; border: 1px solid #334155; color: #e2e8f0; padding: 5px 8px; border-radius: 4px; font-size: 13px; }
|
||||
.status-automated { background: #052e16; color: #4ade80; }
|
||||
.status-partial { background: #451a03; color: #fbbf24; }
|
||||
.status-manual { background: #450a0a; color: #f87171; }
|
||||
.dirty { color: #fbbf24; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="text-sm">
|
||||
<div class="max-w-[1600px] mx-auto">
|
||||
<!-- Header -->
|
||||
<div class="bg-[#020617] border-b border-[#1e293b] px-8 py-5 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold flex items-center gap-3">
|
||||
<i class="fa-solid fa-robot text-orange-400"></i>
|
||||
Autotest
|
||||
</h1>
|
||||
<p class="text-[#64748b] text-xs mt-0.5">Graphical editor for PBX / Core Automation Plan</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<div id="status" class="text-xs px-3 py-1 rounded bg-[#1e2937] text-amber-300 font-medium">
|
||||
<span id="count">0</span> test cases loaded
|
||||
</div>
|
||||
<button onclick="saveAll()"
|
||||
class="px-5 py-2 bg-[#0b1120] text-orange-400 border border-orange-400 hover:bg-orange-900/30 rounded-lg font-semibold flex items-center gap-2 text-sm">
|
||||
<i class="fa-solid fa-save"></i>
|
||||
<span>Save All Changes</span>
|
||||
</button>
|
||||
|
||||
<!-- Report Button (generates grouped summary like the editor) -->
|
||||
<button onclick="generateAndDownloadReport()"
|
||||
class="px-4 py-2 bg-[#0b1120] text-orange-400 border border-orange-400 hover:bg-orange-900/30 rounded-lg text-sm font-semibold flex items-center gap-2">
|
||||
<i class="fa-solid fa-file-alt"></i>
|
||||
<span>Report</span>
|
||||
</button>
|
||||
|
||||
<!-- Generate Test Plan for manual execution -->
|
||||
<button onclick="openGenerateTestPlanModal()"
|
||||
class="px-4 py-2 bg-[#0b1120] text-orange-400 border border-orange-400 hover:bg-orange-900/30 rounded-lg text-sm font-semibold flex items-center gap-2">
|
||||
<i class="fa-solid fa-list-check"></i>
|
||||
<span>Generate Test Plan</span>
|
||||
</button>
|
||||
|
||||
<!-- Load existing Test Plan -->
|
||||
<button onclick="openLoadTestPlanModal()"
|
||||
class="px-4 py-2 bg-[#0b1120] text-orange-400 border border-orange-400 hover:bg-orange-900/30 rounded-lg text-sm font-semibold flex items-center gap-2">
|
||||
<i class="fa-solid fa-folder-open"></i>
|
||||
<span>Load Test Plan</span>
|
||||
</button>
|
||||
|
||||
<button onclick="reloadData()"
|
||||
class="px-4 py-2 bg-[#1e2937] hover:bg-[#334155] rounded-lg text-sm flex items-center gap-2">
|
||||
<i class="fa-solid fa-sync"></i>
|
||||
<span>Reload</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="px-8 pt-6">
|
||||
<div class="card rounded-xl p-4 flex flex-wrap gap-4 items-end">
|
||||
<div>
|
||||
<div class="section-title mb-1">Module</div>
|
||||
<select id="fModule" onchange="renderTable()" class="bg-[#0f172a] border border-[#334155] rounded px-3 py-1.5 text-sm w-40">
|
||||
<option value="">All</option>
|
||||
<option value="core">Core</option>
|
||||
<option value="pbx">PBX</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<div class="section-title mb-1">Group</div>
|
||||
<select id="fGroup" onchange="renderTable()" class="bg-[#0f172a] border border-[#334155] rounded px-3 py-1.5 text-sm w-56">
|
||||
<option value="">All</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<div class="section-title mb-1">Status</div>
|
||||
<select id="fStatus" onchange="renderTable()" class="bg-[#0f172a] border border-[#334155] rounded px-3 py-1.5 text-sm w-40">
|
||||
<option value="">All</option>
|
||||
<option value="automated">Automated</option>
|
||||
<option value="partial">Partial</option>
|
||||
<option value="manual">Manual</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex-1 min-w-[240px]">
|
||||
<div class="section-title mb-1">Search</div>
|
||||
<input id="fSearch" oninput="renderTable()" type="text" placeholder="ID, description, group..."
|
||||
class="w-full bg-[#0f172a] border border-[#334155] rounded px-3 py-1.5 text-sm">
|
||||
</div>
|
||||
<button onclick="resetFilters()"
|
||||
class="px-4 py-1.5 text-sm bg-[#1e2937] hover:bg-[#334155] rounded border border-[#334155]">
|
||||
Clear Filters
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="px-8 pt-4 pb-12">
|
||||
<!-- Selection Toolbar -->
|
||||
<div class="flex items-center gap-2 mb-2 px-1 text-xs">
|
||||
<span class="text-[#64748b]">Selection:</span>
|
||||
<button onclick="selectOnlyManualAndPartial()" class="px-2 py-1 bg-[#1e2937] hover:bg-orange-600 rounded text-orange-300">Manual + Partial</button>
|
||||
<button onclick="selectAllCore()" class="px-2 py-1 bg-[#1e2937] hover:bg-orange-600 rounded">All Core</button>
|
||||
<button onclick="selectAllPbx()" class="px-2 py-1 bg-[#1e2937] hover:bg-orange-600 rounded">All PBX</button>
|
||||
<button onclick="clearSelection()" class="px-2 py-1 bg-[#1e2937] hover:bg-red-600 rounded text-red-300">Clear</button>
|
||||
<span class="ml-3 text-amber-300 font-medium" id="selectionCount"></span>
|
||||
</div>
|
||||
|
||||
<div class="card rounded-xl overflow-hidden">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="bg-[#0f172a] text-[#64748b] text-xs">
|
||||
<th class="w-8 py-3 px-2"></th>
|
||||
<th class="w-10 py-3 px-2 text-left">#</th>
|
||||
<th class="py-3 px-3 text-left">Test ID</th>
|
||||
<th class="w-16 py-3 px-2 text-left">Module</th>
|
||||
<th class="w-16 py-3 px-2 text-left">Action</th>
|
||||
<th class="py-3 px-3 text-left">Description</th>
|
||||
<th class="w-24 py-3 px-2 text-left">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="tableBody" class="divide-y divide-[#334155]"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div id="noResults" class="hidden text-center py-8 text-[#64748b]">No tests match the current filters.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- EDIT MODAL -->
|
||||
<div id="editModal" class="hidden fixed inset-0 bg-black/70 flex items-center justify-center z-50" onclick="if (event.target.id === 'editModal') closeModal()">
|
||||
<div class="modal bg-[#1e2937] border border-[#334155] rounded-2xl w-full max-w-4xl max-h-[92vh] flex flex-col mx-4" onclick="event.stopImmediatePropagation()">
|
||||
<!-- Modal Header -->
|
||||
<div class="px-6 py-4 border-b border-[#334155] flex items-center justify-between bg-[#0f172a] rounded-t-2xl">
|
||||
<div>
|
||||
<div id="modalId" class="font-mono text-sm text-[#94a3b8]"></div>
|
||||
<div id="modalGroup" class="text-xs text-[#64748b]"></div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<select id="modalStatus" class="bg-[#0f172a] border border-[#334155] text-sm rounded px-3 py-1">
|
||||
<option value="automated">Automated</option>
|
||||
<option value="partial">Partial</option>
|
||||
<option value="manual">Manual</option>
|
||||
</select>
|
||||
<button onclick="resetToSuggested()"
|
||||
class="text-xs px-3 py-1 bg-[#0b1120] text-orange-400 border border-orange-400 hover:bg-orange-900/30 rounded flex items-center gap-1 whitespace-nowrap">
|
||||
<i class="fa-solid fa-magic"></i>
|
||||
<span>Reset to suggested</span>
|
||||
</button>
|
||||
<button onclick="closeModal()" class="text-[#64748b] hover:text-white px-2 text-xl">×</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-6 overflow-auto flex-1 space-y-6 text-sm">
|
||||
<!-- Description -->
|
||||
<div>
|
||||
<div class="section-title mb-1.5">Description</div>
|
||||
<textarea id="modalDescription" rows="2"
|
||||
class="w-full bg-[#0f172a] border border-[#334155] rounded-lg p-3 text-sm"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Steps -->
|
||||
<div>
|
||||
<div class="flex justify-between items-center mb-1.5">
|
||||
<div class="section-title">Test Steps</div>
|
||||
<button onclick="addListItem('steps')" class="text-xs px-2.5 py-0.5 bg-[#334155] hover:bg-[#475569] rounded">+ Add step</button>
|
||||
</div>
|
||||
<ul id="listSteps" class="editable-list text-sm"></ul>
|
||||
</div>
|
||||
|
||||
<!-- Automatable -->
|
||||
<div>
|
||||
<div class="flex justify-between items-center mb-1.5">
|
||||
<div class="section-title text-emerald-400">Automatable</div>
|
||||
<button onclick="addListItem('automatable')" class="text-xs px-2.5 py-0.5 bg-emerald-900/60 hover:bg-emerald-800 rounded">+ Add</button>
|
||||
</div>
|
||||
<ul id="listAutomatable" class="editable-list text-sm"></ul>
|
||||
</div>
|
||||
|
||||
<!-- Not Yet -->
|
||||
<div>
|
||||
<div class="flex justify-between items-center mb-1.5">
|
||||
<div class="section-title text-red-400">Not Automatable Yet / Manual</div>
|
||||
<button onclick="addListItem('notYet')" class="text-xs px-2.5 py-0.5 bg-red-900/60 hover:bg-red-800 rounded">+ Add</button>
|
||||
</div>
|
||||
<ul id="listNotYet" class="editable-list text-sm"></ul>
|
||||
</div>
|
||||
|
||||
<!-- Dependencies -->
|
||||
<div>
|
||||
<div class="flex justify-between items-center mb-1.5">
|
||||
<div class="section-title">Dependencies & Prerequisites</div>
|
||||
<button onclick="addDependency()" class="text-xs px-2.5 py-0.5 bg-[#334155] hover:bg-[#475569] rounded">+ Add dependency</button>
|
||||
</div>
|
||||
<div id="depContainer" class="space-y-1"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-6 py-4 border-t border-[#334155] bg-[#0f172a] rounded-b-2xl flex justify-end gap-3">
|
||||
<button onclick="closeModal()" class="px-5 py-2 text-sm rounded-lg bg-[#334155] hover:bg-[#475569]">Cancel</button>
|
||||
<button onclick="saveCurrentTest()" class="px-6 py-2 text-sm rounded-lg bg-sky-600 hover:bg-sky-500 font-semibold">Save Changes to Test</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let allTests = [];
|
||||
let filteredTests = [];
|
||||
let currentTest = null;
|
||||
let currentIndex = -1;
|
||||
let isDirty = false;
|
||||
let selectedIds = new Set(); // For Generate Test Plan selection
|
||||
|
||||
async function loadData() {
|
||||
try {
|
||||
const res = await fetch('/api/testcases');
|
||||
allTests = await res.json();
|
||||
populateGroupFilter();
|
||||
renderTable();
|
||||
updateSelectionCount();
|
||||
document.getElementById('count').textContent = allTests.length;
|
||||
} catch (e) {
|
||||
alert('Could not load test cases. Make sure the server is running and data file exists.');
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
function populateGroupFilter() {
|
||||
const select = document.getElementById('fGroup');
|
||||
const groups = [...new Set(allTests.map(t => t.group))].sort();
|
||||
select.innerHTML = '<option value="">All</option>' + groups.map(g => `<option value="${g}">${g}</option>`).join('');
|
||||
}
|
||||
|
||||
function getFiltered() {
|
||||
const mod = document.getElementById('fModule').value;
|
||||
const grp = document.getElementById('fGroup').value;
|
||||
const stat = document.getElementById('fStatus').value;
|
||||
const search = document.getElementById('fSearch').value.toLowerCase().trim();
|
||||
|
||||
return allTests.filter(t => {
|
||||
if (mod && t.module !== mod) return false;
|
||||
if (grp && t.group !== grp) return false;
|
||||
if (stat && t.status !== stat) return false;
|
||||
if (search) {
|
||||
const hay = (t.id + ' ' + t.description + ' ' + t.group).toLowerCase();
|
||||
if (!hay.includes(search)) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function renderTable() {
|
||||
filteredTests = getFiltered();
|
||||
const tbody = document.getElementById('tableBody');
|
||||
const noRes = document.getElementById('noResults');
|
||||
|
||||
if (filteredTests.length === 0) {
|
||||
tbody.innerHTML = '';
|
||||
noRes.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
noRes.classList.add('hidden');
|
||||
|
||||
// Group by 'group' field (e.g. core-repository, pbx-calls, etc.)
|
||||
const grouped = {};
|
||||
filteredTests.forEach(t => {
|
||||
if (!grouped[t.group]) grouped[t.group] = [];
|
||||
grouped[t.group].push(t);
|
||||
});
|
||||
|
||||
const groupNames = Object.keys(grouped).sort();
|
||||
|
||||
let html = '';
|
||||
|
||||
groupNames.forEach(groupName => {
|
||||
const testsInGroup = grouped[groupName];
|
||||
const allSelectedInGroup = testsInGroup.every(t => selectedIds.has(t.id));
|
||||
|
||||
// Group header
|
||||
html += `
|
||||
<tr class="bg-[#111827]">
|
||||
<td colspan="7" style="padding: 5px 10px; border-bottom: 1px solid #334155;">
|
||||
<label style="display: flex; align-items: center; gap: 8px; cursor: pointer; color: #fb923c; font-weight: 600; font-size: 13px;">
|
||||
<input type="checkbox" ${allSelectedInGroup ? 'checked' : ''}
|
||||
onclick="event.stopImmediatePropagation(); toggleGroupSelection('${groupName}', this.checked)">
|
||||
<span>${groupName}</span>
|
||||
<span style="font-size: 11px; color: #64748b; font-weight: normal;">(${testsInGroup.length} tests)</span>
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
|
||||
testsInGroup.forEach((t, idx) => {
|
||||
const statusClass = `status-${t.status}`;
|
||||
const shortId = t.id.length > 52 ? t.id.slice(0, 49) + '…' : t.id;
|
||||
const isChecked = selectedIds.has(t.id) ? 'checked' : '';
|
||||
|
||||
html += `
|
||||
<tr class="row-hover">
|
||||
<td class="py-1.5 px-2">
|
||||
<input type="checkbox" ${isChecked}
|
||||
onclick="event.stopImmediatePropagation(); toggleSelection('${t.id}', this.checked)">
|
||||
</td>
|
||||
<td class="py-1.5 px-2 text-[#64748b] cursor-pointer text-xs" onclick="openEditor(${allTests.indexOf(t)})">${idx + 1}</td>
|
||||
<td class="py-1.5 px-2 font-mono text-xs text-[#94a3b8] cursor-pointer" onclick="openEditor(${allTests.indexOf(t)})">${shortId}</td>
|
||||
<td class="py-1.5 px-2 cursor-pointer" onclick="openEditor(${allTests.indexOf(t)})">
|
||||
<span class="px-2 py-0.5 text-xs rounded ${t.module === 'core' ? 'bg-blue-900 text-blue-300' : 'bg-violet-900 text-violet-300'}">${t.module}</span>
|
||||
</td>
|
||||
<td class="py-1.5 px-2 text-[#94a3b8] cursor-pointer text-xs" onclick="openEditor(${allTests.indexOf(t)})">${t.action}</td>
|
||||
<td class="py-1.5 px-2 text-[#e2e8f0] cursor-pointer text-sm" onclick="openEditor(${allTests.indexOf(t)})">${t.description}</td>
|
||||
<td class="py-1.5 px-2 cursor-pointer" onclick="openEditor(${allTests.indexOf(t)})">
|
||||
<span class="px-2.5 py-0.5 text-xs font-semibold rounded-full ${statusClass}">${t.status}</span>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
});
|
||||
|
||||
tbody.innerHTML = html;
|
||||
updateSelectionCount();
|
||||
}
|
||||
|
||||
function resetFilters() {
|
||||
document.getElementById('fModule').value = '';
|
||||
document.getElementById('fGroup').value = '';
|
||||
document.getElementById('fStatus').value = '';
|
||||
document.getElementById('fSearch').value = '';
|
||||
renderTable();
|
||||
}
|
||||
|
||||
function toggleSelection(id, checked) {
|
||||
if (checked) {
|
||||
selectedIds.add(id);
|
||||
} else {
|
||||
selectedIds.delete(id);
|
||||
}
|
||||
renderTable();
|
||||
updateSelectionCount();
|
||||
}
|
||||
|
||||
function toggleGroupSelection(groupName, checked) {
|
||||
const testsInGroup = allTests.filter(t => t.group === groupName);
|
||||
testsInGroup.forEach(t => {
|
||||
if (checked) {
|
||||
selectedIds.add(t.id);
|
||||
} else {
|
||||
selectedIds.delete(t.id);
|
||||
}
|
||||
});
|
||||
renderTable();
|
||||
updateSelectionCount();
|
||||
}
|
||||
|
||||
function toggleGroupSelection(groupName, checked) {
|
||||
const testsInGroup = allTests.filter(t => t.group === groupName);
|
||||
|
||||
testsInGroup.forEach(t => {
|
||||
if (checked) {
|
||||
selectedIds.add(t.id);
|
||||
} else {
|
||||
selectedIds.delete(t.id);
|
||||
}
|
||||
});
|
||||
renderTable();
|
||||
updateSelectionCount();
|
||||
}
|
||||
|
||||
// Bulk selection helpers
|
||||
function selectOnlyManualAndPartial() {
|
||||
selectedIds.clear();
|
||||
allTests.forEach(t => {
|
||||
if (t.status === 'manual' || t.status === 'partial') selectedIds.add(t.id);
|
||||
});
|
||||
renderTable();
|
||||
updateSelectionCount();
|
||||
}
|
||||
|
||||
function selectAllCore() {
|
||||
selectedIds.clear();
|
||||
allTests.forEach(t => {
|
||||
if (t.module === 'core') selectedIds.add(t.id);
|
||||
});
|
||||
renderTable();
|
||||
updateSelectionCount();
|
||||
}
|
||||
|
||||
function selectAllPbx() {
|
||||
selectedIds.clear();
|
||||
allTests.forEach(t => {
|
||||
if (t.module === 'pbx') selectedIds.add(t.id);
|
||||
});
|
||||
renderTable();
|
||||
updateSelectionCount();
|
||||
}
|
||||
|
||||
function clearSelection() {
|
||||
selectedIds.clear();
|
||||
renderTable();
|
||||
updateSelectionCount();
|
||||
}
|
||||
|
||||
function updateSelectionCount() {
|
||||
const el = document.getElementById('selectionCount');
|
||||
if (el) {
|
||||
el.textContent = selectedIds.size > 0 ? `${selectedIds.size} selected` : '';
|
||||
}
|
||||
}
|
||||
|
||||
function generateAndDownloadReport() {
|
||||
alert("La función de Report está temporalmente deshabilitada mientras corregimos un error de sintaxis.\n\nEstamos trabajando en una versión estable.");
|
||||
// TODO: Implementar una versión limpia del generador de report
|
||||
}
|
||||
|
||||
async function openLoadTestPlanModal() {
|
||||
try {
|
||||
const res = await fetch('/api/test-plans');
|
||||
const plans = await res.json();
|
||||
|
||||
if (!plans || plans.length === 0) {
|
||||
alert('No saved test plans found.');
|
||||
return;
|
||||
}
|
||||
|
||||
let listHtml = '<div style="max-height:400px; overflow:auto;">';
|
||||
plans.forEach(p => {
|
||||
const date = new Date(p.updatedAt || p.createdAt).toLocaleString();
|
||||
listHtml += `
|
||||
<div onclick="window.open('/tester-plan.html?plan=${p.id}', '_blank'); closeLoadModal();"
|
||||
style="padding:10px; border-bottom:1px solid #334155; cursor:pointer; display:flex; justify-content:space-between;">
|
||||
<div>
|
||||
<strong>${p.name}</strong><br>
|
||||
<small style="color:#64748b">${p.testCount} tests • ${date}</small>
|
||||
</div>
|
||||
<div style="color:#f59e0b">Open →</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
listHtml += '</div>';
|
||||
|
||||
const modal = document.createElement('div');
|
||||
modal.id = 'loadPlanModal';
|
||||
modal.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.7);display:flex;align-items:center;justify-content:center;z-index:9999';
|
||||
modal.innerHTML = `
|
||||
<div style="background:#1e2937; border:1px solid #334155; border-radius:12px; width:90%; max-width:600px; padding:20px;">
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:12px;">
|
||||
<h3 style="margin:0; color:#f59e0b;">Saved Test Plans</h3>
|
||||
<button onclick="closeLoadModal()" style="background:none; border:none; color:#64748b; font-size:20px; cursor:pointer;">×</button>
|
||||
</div>
|
||||
${listHtml}
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(modal);
|
||||
|
||||
} catch (e) {
|
||||
alert('Error loading plans: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
function closeLoadModal() {
|
||||
const m = document.getElementById('loadPlanModal');
|
||||
if (m) m.remove();
|
||||
}
|
||||
|
||||
function openEditor(globalIndex) {
|
||||
currentIndex = globalIndex;
|
||||
currentTest = JSON.parse(JSON.stringify(allTests[globalIndex])); // deep clone
|
||||
|
||||
document.getElementById('modalId').textContent = currentTest.id;
|
||||
document.getElementById('modalGroup').textContent = `${currentTest.group} • ${currentTest.file}`;
|
||||
document.getElementById('modalDescription').value = currentTest.description || '';
|
||||
document.getElementById('modalStatus').value = currentTest.status;
|
||||
|
||||
// Render lists
|
||||
renderList('listSteps', currentTest.steps || []);
|
||||
renderList('listAutomatable', currentTest.automatable || []);
|
||||
renderList('listNotYet', currentTest.notYet || []);
|
||||
renderDependencies(currentTest.dependencies || []);
|
||||
|
||||
document.getElementById('editModal').classList.remove('hidden');
|
||||
document.getElementById('editModal').classList.add('flex');
|
||||
}
|
||||
|
||||
function renderList(containerId, items) {
|
||||
const ul = document.getElementById(containerId);
|
||||
ul.innerHTML = items.map((item, idx) => `
|
||||
<li>
|
||||
<input value="${item.replace(/"/g, '"')}" oninput="updateListItem('${containerId}', ${idx}, this.value)">
|
||||
<button onclick="removeListItem('${containerId}', ${idx})"><i class="fa-solid fa-trash text-xs"></i></button>
|
||||
</li>
|
||||
`).join('');
|
||||
ul.dataset.items = JSON.stringify(items);
|
||||
}
|
||||
|
||||
function updateListItem(containerId, idx, value) {
|
||||
const ul = document.getElementById(containerId);
|
||||
const items = JSON.parse(ul.dataset.items || '[]');
|
||||
items[idx] = value;
|
||||
ul.dataset.items = JSON.stringify(items);
|
||||
}
|
||||
|
||||
function removeListItem(containerId, idx) {
|
||||
const ul = document.getElementById(containerId);
|
||||
const items = JSON.parse(ul.dataset.items || '[]');
|
||||
items.splice(idx, 1);
|
||||
renderList(containerId, items);
|
||||
}
|
||||
|
||||
function addListItem(type) {
|
||||
const map = { steps: 'listSteps', automatable: 'listAutomatable', notYet: 'listNotYet' };
|
||||
const id = map[type];
|
||||
const ul = document.getElementById(id);
|
||||
const items = JSON.parse(ul.dataset.items || '[]');
|
||||
items.push('');
|
||||
renderList(id, items);
|
||||
}
|
||||
|
||||
function renderDependencies(deps) {
|
||||
const container = document.getElementById('depContainer');
|
||||
container.innerHTML = deps.map((d, i) => `
|
||||
<div class="dep-row">
|
||||
<input value="${d.label || ''}" placeholder="Label" style="width:160px" oninput="updateDep(${i}, 'label', this.value)">
|
||||
<input value="${d.value || ''}" placeholder="Value / URL / Requirement" style="flex:1" oninput="updateDep(${i}, 'value', this.value)">
|
||||
<button onclick="removeDep(${i})" class="px-2 text-red-400"><i class="fa-solid fa-trash"></i></button>
|
||||
</div>
|
||||
`).join('');
|
||||
container.dataset.deps = JSON.stringify(deps);
|
||||
}
|
||||
|
||||
function updateDep(idx, field, val) {
|
||||
const container = document.getElementById('depContainer');
|
||||
const deps = JSON.parse(container.dataset.deps || '[]');
|
||||
deps[idx][field] = val;
|
||||
container.dataset.deps = JSON.stringify(deps);
|
||||
}
|
||||
|
||||
function removeDep(idx) {
|
||||
const container = document.getElementById('depContainer');
|
||||
const deps = JSON.parse(container.dataset.deps || '[]');
|
||||
deps.splice(idx, 1);
|
||||
renderDependencies(deps);
|
||||
}
|
||||
|
||||
function addDependency() {
|
||||
const container = document.getElementById('depContainer');
|
||||
const deps = JSON.parse(container.dataset.deps || '[]');
|
||||
deps.push({ label: '', value: '' });
|
||||
renderDependencies(deps);
|
||||
}
|
||||
|
||||
async function openGenerateTestPlanModal() {
|
||||
if (selectedIds.size === 0) {
|
||||
alert('Please select at least one test case using the checkboxes.');
|
||||
return;
|
||||
}
|
||||
|
||||
const baseName = prompt('Enter name for this Test Plan:', 'Manual Test Plan');
|
||||
if (!baseName) return;
|
||||
|
||||
const ids = Array.from(selectedIds);
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/test-plans', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ ids, name: baseName })
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success && data.planId) {
|
||||
// Open the editable tester view in a new tab
|
||||
window.open(`/tester-plan.html?plan=${data.planId}`, '_blank');
|
||||
alert(`✅ Test Plan created!\n\nYou can now open the editable plan in the new tab.\nThe tester can edit results with auto-save.`);
|
||||
} else {
|
||||
alert('Error creating plan: ' + (data.error || 'Unknown error'));
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Failed to create Test Plan: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function resetToSuggested() {
|
||||
if (!currentTest) return;
|
||||
|
||||
// Build a minimal test object from current modal values
|
||||
const tempTest = {
|
||||
id: currentTest.id,
|
||||
group: currentTest.group,
|
||||
module: currentTest.module,
|
||||
action: currentTest.action,
|
||||
description: document.getElementById('modalDescription').value.trim(),
|
||||
status: document.getElementById('modalStatus').value,
|
||||
manualReason: currentTest.manualReason,
|
||||
partialNotes: currentTest.partialNotes
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/enrich', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(tempTest)
|
||||
});
|
||||
const rich = await res.json();
|
||||
|
||||
// Repopulate lists with suggested values
|
||||
currentTest.steps = rich.steps || [];
|
||||
currentTest.automatable = rich.automatable || [];
|
||||
currentTest.notYet = rich.notYet || [];
|
||||
currentTest.dependencies = rich.dependencies || [];
|
||||
|
||||
renderList('listSteps', currentTest.steps);
|
||||
renderList('listAutomatable', currentTest.automatable);
|
||||
renderList('listNotYet', currentTest.notYet);
|
||||
renderDependencies(currentTest.dependencies);
|
||||
|
||||
} catch (e) {
|
||||
alert('Could not fetch suggestions from server');
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
function saveCurrentTest() {
|
||||
if (currentIndex < 0) return;
|
||||
|
||||
// Collect values
|
||||
currentTest.description = document.getElementById('modalDescription').value.trim();
|
||||
currentTest.status = document.getElementById('modalStatus').value;
|
||||
|
||||
// Collect lists
|
||||
const stepsUl = document.getElementById('listSteps');
|
||||
currentTest.steps = JSON.parse(stepsUl.dataset.items || '[]').filter(Boolean);
|
||||
|
||||
const autoUl = document.getElementById('listAutomatable');
|
||||
currentTest.automatable = JSON.parse(autoUl.dataset.items || '[]').filter(Boolean);
|
||||
|
||||
const notUl = document.getElementById('listNotYet');
|
||||
currentTest.notYet = JSON.parse(notUl.dataset.items || '[]').filter(Boolean);
|
||||
|
||||
const depContainer = document.getElementById('depContainer');
|
||||
currentTest.dependencies = JSON.parse(depContainer.dataset.deps || '[]')
|
||||
.filter(d => d.label || d.value);
|
||||
|
||||
// Update master array
|
||||
allTests[currentIndex] = currentTest;
|
||||
isDirty = true;
|
||||
|
||||
closeModal();
|
||||
renderTable();
|
||||
|
||||
document.getElementById('status').classList.add('dirty');
|
||||
document.getElementById('status').innerHTML = '<span class="text-amber-400">Unsaved changes</span>';
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
document.getElementById('editModal').classList.remove('flex');
|
||||
document.getElementById('editModal').classList.add('hidden');
|
||||
currentTest = null;
|
||||
currentIndex = -1;
|
||||
}
|
||||
|
||||
async function saveAll() {
|
||||
const btns = document.querySelectorAll('button');
|
||||
try {
|
||||
const res = await fetch('/api/testcases', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(allTests)
|
||||
});
|
||||
const json = await res.json();
|
||||
if (json.success) {
|
||||
isDirty = false;
|
||||
document.getElementById('status').classList.remove('dirty');
|
||||
document.getElementById('status').innerHTML = `<span id="count">${allTests.length}</span> test cases saved`;
|
||||
alert('✅ All changes saved successfully!');
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Error saving data: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function reloadData() {
|
||||
if (isDirty && !confirm('You have unsaved changes. Reload anyway?')) return;
|
||||
await loadData();
|
||||
isDirty = false;
|
||||
document.getElementById('status').classList.remove('dirty');
|
||||
document.getElementById('status').innerHTML = `<span id="count">${allTests.length}</span> test cases loaded`;
|
||||
}
|
||||
|
||||
// Keyboard shortcuts
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
const modal = document.getElementById('editModal');
|
||||
if (!modal.classList.contains('hidden')) closeModal();
|
||||
}
|
||||
if (e.metaKey && e.key === 's') {
|
||||
e.preventDefault();
|
||||
saveAll();
|
||||
}
|
||||
});
|
||||
|
||||
// Init
|
||||
window.onload = () => {
|
||||
loadData();
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,155 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Tester - Test Plan Editor</title>
|
||||
<style>
|
||||
body { font-family: system-ui, -apple-system, sans-serif; background: #0b1120; color: #e2e8f0; margin: 0; padding: 20px; }
|
||||
.container { max-width: 1400px; margin: 0 auto; }
|
||||
h1 { color: #f59e0b; }
|
||||
.plan-header { background: #1e2937; padding: 16px; border-radius: 8px; margin-bottom: 20px; }
|
||||
.test-card { background: #1e2937; border: 1px solid #334155; border-radius: 8px; padding: 16px; margin-bottom: 16px; }
|
||||
.test-header { font-weight: 600; color: #f59e0b; margin-bottom: 8px; }
|
||||
textarea, select, input { background: #0f172a; border: 1px solid #334155; color: #e2e8f0; padding: 8px; border-radius: 4px; width: 100%; box-sizing: border-box; }
|
||||
.field { margin-bottom: 10px; }
|
||||
.label { font-size: 12px; color: #94a3b8; margin-bottom: 4px; display: block; }
|
||||
.status-pass { color: #4ade80; }
|
||||
.status-fail { color: #f87171; }
|
||||
.auto-save { font-size: 12px; color: #64748b; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="plan-header">
|
||||
<h1 id="planName"></h1>
|
||||
<div id="planMeta" style="font-size:13px; color:#94a3b8;"></div>
|
||||
<div class="auto-save" id="saveStatus"></div>
|
||||
</div>
|
||||
|
||||
<div id="testsContainer"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const planId = urlParams.get('plan');
|
||||
|
||||
let plan = null;
|
||||
let saveTimeout = null;
|
||||
|
||||
async function loadPlan() {
|
||||
if (!planId) {
|
||||
document.body.innerHTML = '<h1>No plan specified. Use ?plan=ID</h1>';
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await fetch(`/api/test-plans/${planId}`);
|
||||
if (!res.ok) {
|
||||
document.body.innerHTML = '<h1>Plan not found</h1>';
|
||||
return;
|
||||
}
|
||||
|
||||
plan = await res.json();
|
||||
document.getElementById('planName').textContent = plan.name;
|
||||
document.getElementById('planMeta').textContent = `Created: ${new Date(plan.createdAt).toLocaleString()} • ${plan.tests.length} tests`;
|
||||
|
||||
renderTests();
|
||||
}
|
||||
|
||||
function renderTests() {
|
||||
const container = document.getElementById('testsContainer');
|
||||
container.innerHTML = '';
|
||||
|
||||
plan.tests.forEach((test, index) => {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'test-card';
|
||||
div.innerHTML = `
|
||||
<div class="test-header">${test.id}</div>
|
||||
<div><strong>${test.description}</strong></div>
|
||||
<div style="margin: 8px 0; font-size:12px; color:#94a3b8;">Group: ${test.group}</div>
|
||||
|
||||
<div class="field">
|
||||
<span class="label">Steps</span>
|
||||
<div style="font-size:12px; white-space: pre-line; background:#0f172a; padding:8px; border-radius:4px;">
|
||||
${(test.steps || []).map(s => '• ' + s).join('\n')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<span class="label">Expected Result</span>
|
||||
<textarea data-field="expectedResult" data-index="${index}">${test.expectedResult || ''}</textarea>
|
||||
</div>
|
||||
|
||||
<div style="display:flex; gap:12px;">
|
||||
<div class="field" style="flex:1;">
|
||||
<span class="label">Severity</span>
|
||||
<select data-field="severity" data-index="${index}">
|
||||
<option ${test.severity === 'Low' ? 'selected' : ''}>Low</option>
|
||||
<option ${test.severity === 'Medium' ? 'selected' : ''}>Medium</option>
|
||||
<option ${test.severity === 'High' ? 'selected' : ''}>High</option>
|
||||
<option ${test.severity === 'Critical' ? 'selected' : ''}>Critical</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field" style="flex:1;">
|
||||
<span class="label">Status</span>
|
||||
<select data-field="testerStatus" data-index="${index}">
|
||||
<option ${test.testerStatus === 'N/A' ? 'selected' : ''}>N/A</option>
|
||||
<option ${test.testerStatus === 'Pass' ? 'selected' : ''}>Pass</option>
|
||||
<option ${test.testerStatus === 'Fail' ? 'selected' : ''}>Fail</option>
|
||||
<option ${test.testerStatus === 'Blocked' ? 'selected' : ''}>Blocked</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<span class="label">Actual Result / Comments</span>
|
||||
<textarea data-field="actualResult" data-index="${index}" style="min-height:60px;">${test.actualResult || ''}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<span class="label">Additional Comments</span>
|
||||
<textarea data-field="comments" data-index="${index}">${test.comments || ''}</textarea>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Attach auto-save listeners
|
||||
div.querySelectorAll('textarea, select').forEach(el => {
|
||||
el.addEventListener('input', () => scheduleSave(index, el.dataset.field, el.value));
|
||||
el.addEventListener('change', () => scheduleSave(index, el.dataset.field, el.value));
|
||||
});
|
||||
|
||||
container.appendChild(div);
|
||||
});
|
||||
}
|
||||
|
||||
function scheduleSave(testIndex, field, value) {
|
||||
plan.tests[testIndex][field] = value;
|
||||
|
||||
document.getElementById('saveStatus').textContent = 'Saving...';
|
||||
|
||||
clearTimeout(saveTimeout);
|
||||
saveTimeout = setTimeout(async () => {
|
||||
try {
|
||||
const res = await fetch(`/api/test-plans/${planId}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(plan)
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
document.getElementById('saveStatus').textContent = `Auto-saved at ${new Date().toLocaleTimeString()}`;
|
||||
setTimeout(() => {
|
||||
const el = document.getElementById('saveStatus');
|
||||
if (el) el.textContent = '';
|
||||
}, 2000);
|
||||
}
|
||||
} catch (e) {
|
||||
document.getElementById('saveStatus').textContent = 'Save failed';
|
||||
}
|
||||
}, 800);
|
||||
}
|
||||
|
||||
loadPlan();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user