arreglos de grok

This commit is contained in:
2026-05-22 13:25:01 +02:00
parent 2ebd8fb16b
commit 8f7f14c95b
641 changed files with 78143 additions and 941 deletions
+731
View File
@@ -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 &amp; 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, '&quot;')}" 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>