Files
autotest/editor/public/index.html
T
2026-05-22 13:25:01 +02:00

732 lines
29 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>